PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/ruby_units/unit.rb

https://github.com/dannemanne/ruby-units
Ruby | 1572 lines | 1169 code | 90 blank | 313 comment | 155 complexity | 7a5340313c7327628be71015a7f3081c MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. require 'date'
  2. if RUBY_VERSION < "1.9"
  3. # :nocov_19:
  4. require 'parsedate'
  5. require 'rational'
  6. # :nocov_19:
  7. end
  8. # Copyright 2006-2012
  9. #
  10. # @author Kevin C. Olbrich, Ph.D.
  11. # @see https://github.com/olbrich/ruby-units
  12. #
  13. # @note The accuracy of unit conversions depends on the precision of the conversion factor.
  14. # If you have more accurate estimates for particular conversion factors, please send them
  15. # to me and I will incorporate them into the next release. It is also incumbent on the end-user
  16. # to ensure that the accuracy of any conversions is sufficient for their intended application.
  17. #
  18. # While there are a large number of unit specified in the base package,
  19. # there are also a large number of units that are not included.
  20. # This package covers nearly all SI, Imperial, and units commonly used
  21. # in the United States. If your favorite units are not listed here, file an issue on github.
  22. #
  23. # To add or override a unit definition, add a code block like this..
  24. # @example Define a new unit
  25. # Unit.define("foobar") do |unit|
  26. # unit.aliases = %w{foo fb foo-bar}
  27. # unit.definition = Unit("1 baz")
  28. # end
  29. #
  30. # @todo fix class variables so they conform to standard naming conventions and refactor away as many of them as possible
  31. # @todo pull caching out into its own class
  32. # @todo refactor internal representation of units
  33. # @todo method to determine best natural prefix
  34. class Unit < Numeric
  35. VERSION = Unit::Version::STRING
  36. @@definitions = {}
  37. @@PREFIX_VALUES = {}
  38. @@PREFIX_MAP = {}
  39. @@UNIT_MAP = {}
  40. @@UNIT_VALUES = {}
  41. @@UNIT_REGEX = nil
  42. @@UNIT_MATCH_REGEX = nil
  43. UNITY = '<1>'
  44. UNITY_ARRAY = [UNITY]
  45. FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/
  46. TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/
  47. LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds|pound-mass)+[\s,]*(\d+)\s*(?:oz|ounces)/
  48. SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)}
  49. RATIONAL_NUMBER = /\(?([+-]?\d+)\/(\d+)\)?/
  50. COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/
  51. NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/
  52. UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/
  53. TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/
  54. BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/
  55. UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/
  56. COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/
  57. RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/
  58. KELVIN = ['<kelvin>']
  59. FAHRENHEIT = ['<fahrenheit>']
  60. RANKINE = ['<rankine>']
  61. CELSIUS = ['<celsius>']
  62. @@TEMP_REGEX = nil
  63. SIGNATURE_VECTOR = [
  64. :length,
  65. :time,
  66. :temperature,
  67. :mass,
  68. :current,
  69. :substance,
  70. :luminosity,
  71. :currency,
  72. :memory,
  73. :angle
  74. ]
  75. @@KINDS = {
  76. -312078 => :elastance,
  77. -312058 => :resistance,
  78. -312038 => :inductance,
  79. -152040 => :magnetism,
  80. -152038 => :magnetism,
  81. -152058 => :potential,
  82. -7997 => :specific_volume,
  83. -79 => :snap,
  84. -59 => :jolt,
  85. -39 => :acceleration,
  86. -38 => :radiation,
  87. -20 => :frequency,
  88. -19 => :speed,
  89. -18 => :viscosity,
  90. -17 => :volumetric_flow,
  91. -1 => :wavenumber,
  92. 0 => :unitless,
  93. 1 => :length,
  94. 2 => :area,
  95. 3 => :volume,
  96. 20 => :time,
  97. 400 => :temperature,
  98. 7941 => :yank,
  99. 7942 => :power,
  100. 7959 => :pressure,
  101. 7962 => :energy,
  102. 7979 => :viscosity,
  103. 7961 => :force,
  104. 7981 => :momentum,
  105. 7982 => :angular_momentum,
  106. 7997 => :density,
  107. 7998 => :area_density,
  108. 8000 => :mass,
  109. 152020 => :radiation_exposure,
  110. 159999 => :magnetism,
  111. 160000 => :current,
  112. 160020 => :charge,
  113. 312058 => :resistance,
  114. 312078 => :capacitance,
  115. 3199980 => :activity,
  116. 3199997 => :molar_concentration,
  117. 3200000 => :substance,
  118. 63999998 => :illuminance,
  119. 64000000 => :luminous_power,
  120. 1280000000 => :currency,
  121. 25600000000 => :memory,
  122. 511999999980 => :angular_velocity,
  123. 512000000000 => :angle
  124. }
  125. @@cached_units = {}
  126. @@base_unit_cache = {}
  127. # setup internal arrays and hashes
  128. # @return [true]
  129. def self.setup
  130. self.clear_cache
  131. @@PREFIX_VALUES = {}
  132. @@PREFIX_MAP = {}
  133. @@UNIT_VALUES = {}
  134. @@UNIT_MAP = {}
  135. @@UNIT_REGEX = nil
  136. @@UNIT_MATCH_REGEX = nil
  137. @@PREFIX_REGEX = nil
  138. @@definitions.each do |name, definition|
  139. self.use_definition(definition)
  140. end
  141. Unit.new(1)
  142. return true
  143. end
  144. # determine if a unit is already defined
  145. # @param [String] unit
  146. # @return [Boolean]
  147. def self.defined?(unit)
  148. return @@UNIT_VALUES.keys.include?("<#{unit}>")
  149. end
  150. # return the unit definition for a unit
  151. # @param [String] unit
  152. # @return [Unit::Definition, nil]
  153. def self.definition(_unit)
  154. unit = (_unit =~ /^<.+>$/) ? _unit : "<#{_unit}>"
  155. return @@definitions[unit]
  156. end
  157. # return a list of all defined units
  158. # @return [Array]
  159. def self.definitions
  160. return @@definitions
  161. end
  162. # @param [Unit::Definition|String] unit_definition
  163. # @param [Block] block
  164. # @return [Unit::Definition]
  165. # @raise [ArgumentError] when passed a non-string if using the block form
  166. # Unpack a unit definition and add it to the array of defined units
  167. #
  168. # @example Block form
  169. # Unit.define('foobar') do |foobar|
  170. # foobar.definition = Unit("1 baz")
  171. # end
  172. #
  173. # @example Unit::Definition form
  174. # unit_definition = Unit::Definition.new("foobar") {|foobar| foobar.definition = Unit("1 baz")}
  175. # Unit.define(unit_definition)
  176. def self.define(unit_definition, &block)
  177. if block_given?
  178. raise ArgumentError, "When using the block form of Unit.define, pass the name of the unit" unless unit_definition.instance_of?(String)
  179. unit_definition = Unit::Definition.new(unit_definition, &block)
  180. end
  181. Unit.definitions[unit_definition.name] = unit_definition
  182. Unit.use_definition(unit_definition)
  183. return unit_definition
  184. end
  185. # @param [String] name Name of unit to redefine
  186. # @param [Block] block
  187. # @raise [ArgumentError] if a block is not given
  188. # @yield [Unit::Definition]
  189. # @return (see Unit.define)
  190. # Get the definition for a unit and allow it to be redefined
  191. def self.redefine!(name, &block)
  192. raise ArgumentError, "A block is required to redefine a unit" unless block_given?
  193. unit_definition = self.definition(name)
  194. yield unit_definition
  195. self.define(unit_definition)
  196. end
  197. # @param [String] name of unit to undefine
  198. # @return (see Unit.setup)
  199. # Undefine a unit. Will not raise an exception for unknown units.
  200. def self.undefine!(unit)
  201. @@definitions.delete("<#{unit}>")
  202. Unit.setup
  203. end
  204. include Comparable
  205. # @return [Numeric]
  206. attr_accessor :scalar
  207. # @return [Array]
  208. attr_accessor :numerator
  209. # @return [Array]
  210. attr_accessor :denominator
  211. # @return [Integer]
  212. attr_accessor :signature
  213. # @return [Numeric]
  214. attr_accessor :base_scalar
  215. # @return [Array]
  216. attr_accessor :base_numerator
  217. # @return [Array]
  218. attr_accessor :base_denominator
  219. # @return [String]
  220. attr_accessor :output
  221. # @return [String]
  222. attr_accessor :unit_name
  223. # needed to make complex units play nice -- otherwise not detected as a complex_generic
  224. # @param [Class]
  225. # @return [Boolean]
  226. def kind_of?(klass)
  227. self.scalar.kind_of?(klass)
  228. end
  229. # Used to copy one unit to another
  230. # @param [Unit] from Unit to copy defintion from
  231. # @return [Unit]
  232. def copy(from)
  233. @scalar = from.scalar
  234. @numerator = from.numerator
  235. @denominator = from.denominator
  236. @is_base = from.is_base?
  237. @signature = from.signature
  238. @base_scalar = from.base_scalar
  239. @unit_name = from.unit_name rescue nil
  240. return self
  241. end
  242. if RUBY_VERSION < "1.9"
  243. # :nocov_19:
  244. # a list of properties to emit to yaml
  245. # @return [Array]
  246. def to_yaml_properties
  247. %w{@scalar @numerator @denominator @signature @base_scalar}
  248. end
  249. # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
  250. # before YAML'izing it.
  251. # @param [Hash] opts
  252. # @return [String]
  253. def to_yaml( opts = {} )
  254. YAML::quick_emit( object_id, opts ) do |out|
  255. out.map( taguri, to_yaml_style ) do |map|
  256. for m in to_yaml_properties do
  257. map.add( m[1..-1], instance_variable_get( m ) )
  258. end
  259. end
  260. end
  261. end
  262. # :nocov_19:
  263. end
  264. # Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime
  265. #
  266. # @example Valid options include:
  267. # "5.6 kg*m/s^2"
  268. # "5.6 kg*m*s^-2"
  269. # "5.6 kilogram*meter*second^-2"
  270. # "2.2 kPa"
  271. # "37 degC"
  272. # "1" -- creates a unitless constant with value 1
  273. # "GPa" -- creates a unit with scalar 1 with units 'GPa'
  274. # "6'4\""" -- recognized as 6 feet + 4 inches
  275. # "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces
  276. # [1, 'kg']
  277. # {:scalar => 1, :numerator=>'kg'}
  278. #
  279. # @param [Unit,String,Hash,Array,Date,Time,DateTime] options
  280. # @return [Unit]
  281. # @raise [ArgumentError] if absolute value of a temperature is less than absolute zero
  282. # @raise [ArgumentError] if no unit is specified
  283. # @raise [ArgumentError] if an invalid unit is specified
  284. def initialize(*options)
  285. @scalar = nil
  286. @base_scalar = nil
  287. @unit_name = nil
  288. @signature = nil
  289. @output = {}
  290. raise ArgumentError, "Invalid Unit Format" if options[0].nil?
  291. if options.size == 2
  292. # options[0] is the scalar
  293. # options[1] is a unit string
  294. begin
  295. cached = @@cached_units[options[1]] * options[0]
  296. copy(cached)
  297. rescue
  298. initialize("#{options[0]} #{(options[1].units rescue options[1])}")
  299. end
  300. return
  301. end
  302. if options.size == 3
  303. options[1] = options[1].join if options[1].kind_of?(Array)
  304. options[2] = options[2].join if options[2].kind_of?(Array)
  305. begin
  306. cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
  307. copy(cached)
  308. rescue
  309. initialize("#{options[0]} #{options[1]}/#{options[2]}")
  310. end
  311. return
  312. end
  313. case options[0]
  314. when Hash
  315. @scalar = options[0][:scalar] || 1
  316. @numerator = options[0][:numerator] || UNITY_ARRAY
  317. @denominator = options[0][:denominator] || UNITY_ARRAY
  318. @signature = options[0][:signature]
  319. when Array
  320. initialize(*options[0])
  321. return
  322. when Numeric
  323. @scalar = options[0]
  324. @numerator = @denominator = UNITY_ARRAY
  325. when Time
  326. @scalar = options[0].to_f
  327. @numerator = ['<second>']
  328. @denominator = UNITY_ARRAY
  329. when DateTime, Date
  330. @scalar = options[0].ajd
  331. @numerator = ['<day>']
  332. @denominator = UNITY_ARRAY
  333. when /^\s*$/
  334. raise ArgumentError, "No Unit Specified"
  335. when String
  336. parse(options[0])
  337. else
  338. raise ArgumentError, "Invalid Unit Format"
  339. end
  340. self.update_base_scalar
  341. raise ArgumentError, "Temperatures must not be less than absolute zero" if self.is_temperature? && self.base_scalar < 0
  342. unary_unit = self.units || ""
  343. if options.first.instance_of?(String)
  344. opt_scalar, opt_units = Unit.parse_into_numbers_and_units(options[0])
  345. unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(#{Unit.temp_regex})|(pounds|lbs[ ,]\d+ ounces|oz)|('\d+")|(ft|feet[ ,]\d+ in|inch|inches)|%|(#{TIME_REGEX})|i\s?(.+)?|&plusmn;|\+\/-/)
  346. @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty?
  347. end
  348. end
  349. unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{Unit.temp_regex}/) then
  350. @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
  351. end
  352. [@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze}
  353. return self
  354. end
  355. # @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each'
  356. # return the kind of the unit (:mass, :length, etc...)
  357. # @return [Symbol]
  358. def kind
  359. return @@KINDS[self.signature]
  360. end
  361. # @private
  362. # @return [Hash]
  363. def self.cached
  364. return @@cached_units
  365. end
  366. # @private
  367. # @return [true]
  368. def self.clear_cache
  369. @@cached_units = {}
  370. @@base_unit_cache = {}
  371. Unit.new(1)
  372. return true
  373. end
  374. # @private
  375. # @return [Hash]
  376. def self.base_unit_cache
  377. return @@base_unit_cache
  378. end
  379. # @example parse strings
  380. # "1 minute in seconds"
  381. # @param [String] input
  382. # @return [Unit]
  383. def self.parse(input)
  384. first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first
  385. return second.nil? ? first.unit : first.unit.convert_to(second)
  386. end
  387. # @return [Unit]
  388. def to_unit
  389. self
  390. end
  391. alias :unit :to_unit
  392. # Is this unit in base form?
  393. # @return [Boolean]
  394. def is_base?
  395. return @is_base if defined? @is_base
  396. @is_base = (@numerator + @denominator).compact.uniq.
  397. map {|unit| Unit.definition(unit)}.
  398. all? {|element| element.unity? || element.base? }
  399. return @is_base
  400. end
  401. alias :base? :is_base?
  402. # convert to base SI units
  403. # results of the conversion are cached so subsequent calls to this will be fast
  404. # @return [Unit]
  405. # @todo this is brittle as it depends on the display_name of a unit, which can be changed
  406. def to_base
  407. return self if self.is_base?
  408. if @@UNIT_MAP[self.units] =~ /\A<(?:temp|deg)[CRF]>\Z/
  409. if RUBY_VERSION < "1.9"
  410. # :nocov_19:
  411. @signature = @@KINDS.index(:temperature)
  412. # :nocov_19:
  413. else
  414. #:nocov:
  415. @signature = @@KINDS.key(:temperature)
  416. #:nocov:
  417. end
  418. base = case
  419. when self.is_temperature?
  420. self.convert_to('tempK')
  421. when self.is_degree?
  422. self.convert_to('degK')
  423. end
  424. return base
  425. end
  426. cached = ((@@base_unit_cache[self.units] * self.scalar) rescue nil)
  427. return cached if cached
  428. num = []
  429. den = []
  430. q = 1
  431. for unit in @numerator.compact do
  432. if @@PREFIX_VALUES[unit]
  433. q *= @@PREFIX_VALUES[unit]
  434. else
  435. q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
  436. num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
  437. den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
  438. end
  439. end
  440. for unit in @denominator.compact do
  441. if @@PREFIX_VALUES[unit]
  442. q /= @@PREFIX_VALUES[unit]
  443. else
  444. q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
  445. den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
  446. num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
  447. end
  448. end
  449. num = num.flatten.compact
  450. den = den.flatten.compact
  451. num = UNITY_ARRAY if num.empty?
  452. base= Unit.new(Unit.eliminate_terms(q,num,den))
  453. @@base_unit_cache[self.units]=base
  454. return base * @scalar
  455. end
  456. alias :base :to_base
  457. # Generate human readable output.
  458. # If the name of a unit is passed, the unit will first be converted to the target unit before output.
  459. # some named conversions are available
  460. #
  461. # @example
  462. # unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4")
  463. # unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
  464. #
  465. # You can also pass a standard format string (i.e., '%0.2f')
  466. # or a strftime format string.
  467. #
  468. # output is cached so subsequent calls for the same format will be fast
  469. #
  470. # @param [Symbol] target_units
  471. # @return [String]
  472. def to_s(target_units=nil)
  473. out = @output[target_units]
  474. if out
  475. return out
  476. else
  477. case target_units
  478. when :ft
  479. inches = self.convert_to("in").scalar.to_int
  480. out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
  481. when :lbs
  482. ounces = self.convert_to("oz").scalar.to_int
  483. out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
  484. when String
  485. out = case target_units
  486. when /(%[\-+\.\w#]+)\s*(.+)*/ #format string like '%0.2f in'
  487. begin
  488. if $2 #unit specified, need to convert
  489. self.convert_to($2).to_s($1)
  490. else
  491. "#{$1 % @scalar} #{$2 || self.units}".strip
  492. end
  493. rescue # parse it like a strftime format string
  494. (DateTime.new(0) + self).strftime(target_units)
  495. end
  496. when /(\S+)/ #unit only 'mm' or '1/mm'
  497. self.convert_to($1).to_s
  498. else
  499. raise "unhandled case"
  500. end
  501. else
  502. out = case @scalar
  503. when Rational
  504. "#{@scalar} #{self.units}"
  505. else
  506. "#{'%g' % @scalar} #{self.units}"
  507. end.strip
  508. end
  509. @output[target_units] = out
  510. return out
  511. end
  512. end
  513. # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump'
  514. # @deprecated
  515. # @return [String]
  516. def inspect(option=nil)
  517. return super() if option == :dump
  518. return self.to_s
  519. end
  520. # true if unit is a 'temperature', false if a 'degree' or anything else
  521. # @return [Boolean]
  522. # @todo use unit definition to determine if it's a temperature instead of a regex
  523. def is_temperature?
  524. return self.is_degree? && (!(@@UNIT_MAP[self.units] =~ /temp[CFRK]/).nil?)
  525. end
  526. alias :temperature? :is_temperature?
  527. # true if a degree unit or equivalent.
  528. # @return [Boolean]
  529. def is_degree?
  530. return self.kind == :temperature
  531. end
  532. alias :degree? :is_degree?
  533. # returns the 'degree' unit associated with a temperature unit
  534. # @example '100 tempC'.unit.temperature_scale #=> 'degC'
  535. # @return [String] possible values: degC, degF, degR, or degK
  536. def temperature_scale
  537. return nil unless self.is_temperature?
  538. return "deg#{@@UNIT_MAP[self.units][/temp([CFRK])/,1]}"
  539. end
  540. # returns true if no associated units
  541. # false, even if the units are "unitless" like 'radians, each, etc'
  542. # @return [Boolean]
  543. def unitless?
  544. return(@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
  545. end
  546. # Compare two Unit objects. Throws an exception if they are not of compatible types.
  547. # Comparisons are done based on the value of the unit in base SI units.
  548. # @param [Object] other
  549. # @return [-1|0|1|nil]
  550. # @raise [NoMethodError] when other does not define <=>
  551. # @raise [ArgumentError] when units are not compatible
  552. def <=>(other)
  553. case
  554. when !self.base_scalar.respond_to?(:<=>)
  555. raise NoMethodError, "undefined method `<=>' for #{self.base_scalar.inspect}"
  556. when other.nil?
  557. return self.base_scalar <=> nil
  558. when !self.is_temperature? && other.zero?
  559. return self.base_scalar <=> 0
  560. when other.instance_of?(Unit)
  561. raise ArgumentError, "Incompatible Units (#{self.units} !~ #{other.units})" unless self =~ other
  562. return self.base_scalar <=> other.base_scalar
  563. else
  564. x,y = coerce(other)
  565. return x <=> y
  566. end
  567. end
  568. # Compare Units for equality
  569. # this is necessary mostly for Complex units. Complex units do not have a <=> operator
  570. # so we define this one here so that we can properly check complex units for equality.
  571. # Units of incompatible types are not equal, except when they are both zero and neither is a temperature
  572. # Equality checks can be tricky since round off errors may make essentially equivalent units
  573. # appear to be different.
  574. # @param [Object] other
  575. # @return [Boolean]
  576. def ==(other)
  577. case
  578. when other.respond_to?(:zero?) && other.zero?
  579. return self.zero?
  580. when other.instance_of?(Unit)
  581. return false unless self =~ other
  582. return self.base_scalar == other.base_scalar
  583. else
  584. begin
  585. x,y = coerce(other)
  586. return x == y
  587. rescue ArgumentError # return false when object cannot be coerced
  588. return false
  589. end
  590. end
  591. end
  592. # check to see if units are compatible, but not the scalar part
  593. # this check is done by comparing signatures for performance reasons
  594. # if passed a string, it will create a unit object with the string and then do the comparison
  595. # @example this permits a syntax like:
  596. # unit =~ "mm"
  597. # @note if you want to do a regexp comparison of the unit string do this ...
  598. # unit.units =~ /regexp/
  599. # @param [Object] other
  600. # @return [Boolean]
  601. def =~(other)
  602. case other
  603. when Unit
  604. self.signature == other.signature
  605. else
  606. begin
  607. x,y = coerce(other)
  608. return x =~ y
  609. rescue ArgumentError
  610. return false
  611. end
  612. end
  613. end
  614. alias :compatible? :=~
  615. alias :compatible_with? :=~
  616. # Compare two units. Returns true if quantities and units match
  617. # @example
  618. # Unit("100 cm") === Unit("100 cm") # => true
  619. # Unit("100 cm") === Unit("1 m") # => false
  620. # @param [Object] other
  621. # @return [Boolean]
  622. def ===(other)
  623. case other
  624. when Unit
  625. (self.scalar == other.scalar) && (self.units == other.units)
  626. else
  627. begin
  628. x,y = coerce(other)
  629. return x === y
  630. rescue ArgumentError
  631. return false
  632. end
  633. end
  634. end
  635. alias :same? :===
  636. alias :same_as? :===
  637. # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
  638. # throws an exception if the units are not compatible.
  639. # It is possible to add Time objects to units of time
  640. # @param [Object] other
  641. # @return [Unit]
  642. # @raise [ArgumentError] when two temperatures are added
  643. # @raise [ArgumentError] when units are not compatible
  644. # @raise [ArgumentError] when adding a fixed time or date to a time span
  645. def +(other)
  646. case other
  647. when Unit
  648. case
  649. when self.zero?
  650. other.dup
  651. when self =~ other
  652. raise ArgumentError, "Cannot add two temperatures" if ([self, other].all? {|x| x.is_temperature?})
  653. if [self, other].any? {|x| x.is_temperature?}
  654. if self.is_temperature?
  655. Unit.new(:scalar => (self.scalar + other.convert_to(self.temperature_scale).scalar), :numerator => @numerator, :denominator=>@denominator, :signature => @signature)
  656. else
  657. Unit.new(:scalar => (other.scalar + self.convert_to(other.temperature_scale).scalar), :numerator => other.numerator, :denominator=>other.denominator, :signature => other.signature)
  658. end
  659. else
  660. @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.to_base.scalar))
  661. Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
  662. end
  663. else
  664. raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
  665. end
  666. when Date, Time
  667. raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit"
  668. else
  669. x,y = coerce(other)
  670. y + x
  671. end
  672. end
  673. # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
  674. # @param [Numeric] other
  675. # @return [Unit]
  676. # @raise [ArgumentError] when subtracting a temperature from a degree
  677. # @raise [ArgumentError] when units are not compatible
  678. # @raise [ArgumentError] when subtracting a fixed time from a time span
  679. def -(other)
  680. case other
  681. when Unit
  682. case
  683. when self.zero?
  684. -other.dup
  685. when self =~ other
  686. case
  687. when [self, other].all? {|x| x.is_temperature?}
  688. Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self.temperature_scale)
  689. when self.is_temperature?
  690. Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<tempK>'], :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self)
  691. when other.is_temperature?
  692. raise ArgumentError, "Cannot subtract a temperature from a differential degree unit"
  693. else
  694. @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.scalar/self.units.unit.to_base.scalar))
  695. Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
  696. end
  697. else
  698. raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
  699. end
  700. when Time
  701. raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from to a Unit, which can only represent time spans"
  702. else
  703. x,y = coerce(other)
  704. return y-x
  705. end
  706. end
  707. # Multiply two units.
  708. # @param [Numeric] other
  709. # @return [Unit]
  710. # @raise [ArgumentError] when attempting to multiply two temperatures
  711. def *(other)
  712. case other
  713. when Unit
  714. raise ArgumentError, "Cannot multiply by temperatures" if [other,self].any? {|x| x.is_temperature?}
  715. opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)
  716. opts.merge!(:signature => @signature + other.signature)
  717. return Unit.new(opts)
  718. when Numeric
  719. return Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
  720. else
  721. x,y = coerce(other)
  722. return x * y
  723. end
  724. end
  725. # Divide two units.
  726. # Throws an exception if divisor is 0
  727. # @param [Numeric] other
  728. # @return [Unit]
  729. # @raise [ZeroDivisionError] if divisor is zero
  730. # @raise [ArgumentError] if attempting to divide a temperature by another temperature
  731. def /(other)
  732. case other
  733. when Unit
  734. raise ZeroDivisionError if other.zero?
  735. raise ArgumentError, "Cannot divide with temperatures" if [other,self].any? {|x| x.is_temperature?}
  736. opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)
  737. opts.merge!(:signature=> @signature - other.signature)
  738. return Unit.new(opts)
  739. when Numeric
  740. raise ZeroDivisionError if other.zero?
  741. return Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
  742. else
  743. x,y = coerce(other)
  744. return y / x
  745. end
  746. end
  747. # divide two units and return quotient and remainder
  748. # when both units are in the same units we just use divmod on the raw scalars
  749. # otherwise we use the scalar of the base unit which will be a float
  750. # @param [Object] other
  751. # @return [Array]
  752. def divmod(other)
  753. raise ArgumentError, "Incompatible Units" unless self =~ other
  754. if self.units == other.units
  755. return self.scalar.divmod(other.scalar)
  756. else
  757. return self.to_base.scalar.divmod(other.to_base.scalar)
  758. end
  759. end
  760. # perform a modulo on a unit, will raise an exception if the units are not compatible
  761. # @param [Object] other
  762. # @return [Integer]
  763. def %(other)
  764. return self.divmod(other).last
  765. end
  766. # Exponentiate. Only takes integer powers.
  767. # Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
  768. # Throws an exception if exponent is not an integer.
  769. # Ideally this routine should accept a float for the exponent
  770. # It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
  771. # but, sadly, floats can't be converted to rationals.
  772. #
  773. # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1
  774. # @param [Numeric] other
  775. # @return [Unit]
  776. # @raise [ArgumentError] when raising a temperature to a power
  777. # @raise [ArgumentError] when n not in the set integers from (1..9)
  778. # @raise [ArgumentError] when attempting to raise to a complex number
  779. # @raise [ArgumentError] when an invalid exponent is passed
  780. def **(other)
  781. raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
  782. if other.kind_of?(Numeric)
  783. return self.inverse if other == -1
  784. return self if other == 1
  785. return 1 if other.zero?
  786. end
  787. case other
  788. when Rational
  789. return self.power(other.numerator).root(other.denominator)
  790. when Integer
  791. return self.power(other)
  792. when Float
  793. return self**(other.to_i) if other == other.to_i
  794. valid = (1..9).map {|x| 1/x}
  795. raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
  796. return self.root((1/other).to_int)
  797. when (!defined?(Complex).nil? && Complex)
  798. raise ArgumentError, "exponentiation of complex numbers is not yet supported."
  799. else
  800. raise ArgumentError, "Invalid Exponent"
  801. end
  802. end
  803. # returns the unit raised to the n-th power
  804. # @param [Integer] n
  805. # @return [Unit]
  806. # @raise [ArgumentError] when attempting to raise a temperature to a power
  807. # @raise [ArgumentError] when n is not an integer
  808. def power(n)
  809. raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
  810. raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
  811. return self.inverse if n == -1
  812. return 1 if n.zero?
  813. return self if n == 1
  814. if n > 0 then
  815. return (1..(n-1).to_i).inject(self) {|product, x| product * self}
  816. else
  817. return (1..-(n-1).to_i).inject(self) {|product, x| product / self}
  818. end
  819. end
  820. # Calculates the n-th root of a unit
  821. # if n < 0, returns 1/unit^(1/n)
  822. # @param [Integer] n
  823. # @return [Unit]
  824. # @raise [ArgumentError] when attemptint to take the root of a temperature
  825. # @raise [ArgumentError] when n is not an integer
  826. # @raise [ArgumentError] when n is 0
  827. def root(n)
  828. raise ArgumentError, "Cannot take the root of a temperature" if self.is_temperature?
  829. raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
  830. raise ArgumentError, "0th root undefined" if n.zero?
  831. return self if n == 1
  832. return self.root(n.abs).inverse if n < 0
  833. vec = self.unit_signature_vector
  834. vec=vec.map {|x| x % n}
  835. raise ArgumentError, "Illegal root" unless vec.max == 0
  836. num = @numerator.dup
  837. den = @denominator.dup
  838. for item in @numerator.uniq do
  839. x = num.find_all {|i| i==item}.size
  840. r = ((x/n)*(n-1)).to_int
  841. r.times {|y| num.delete_at(num.index(item))}
  842. end
  843. for item in @denominator.uniq do
  844. x = den.find_all {|i| i==item}.size
  845. r = ((x/n)*(n-1)).to_int
  846. r.times {|y| den.delete_at(den.index(item))}
  847. end
  848. q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n)
  849. return Unit.new(:scalar=>q,:numerator=>num,:denominator=>den)
  850. end
  851. # returns inverse of Unit (1/unit)
  852. # @return [Unit]
  853. def inverse
  854. return Unit("1") / self
  855. end
  856. # convert to a specified unit string or to the same units as another Unit
  857. #
  858. # unit.convert_to "kg" will covert to kilograms
  859. # unit1.convert_to unit2 converts to same units as unit2 object
  860. #
  861. # To convert a Unit object to match another Unit object, use:
  862. # unit1 >>= unit2
  863. #
  864. # Special handling for temperature conversions is supported. If the Unit object is converted
  865. # from one temperature unit to another, the proper temperature offsets will be used.
  866. # Supports Kelvin, Celsius, Fahrenheit, and Rankine scales.
  867. #
  868. # @note If temperature is part of a compound unit, the temperature will be treated as a differential
  869. # and the units will be scaled appropriately.
  870. # @param [Object] other
  871. # @return [Unit]
  872. # @raise [ArgumentError] when attempting to convert a degree to a temperature
  873. # @raise [ArgumentError] when target unit is unknown
  874. # @raise [ArgumentError] when target unit is incompatible
  875. def convert_to(other)
  876. return self if other.nil?
  877. return self if TrueClass === other
  878. return self if FalseClass === other
  879. if (Unit === other && other.is_temperature?) || (String === other && other =~ /temp[CFRK]/)
  880. raise ArgumentError, "Receiver is not a temperature unit" unless self.degree?
  881. start_unit = self.units
  882. target_unit = other.units rescue other
  883. unless @base_scalar
  884. @base_scalar = case @@UNIT_MAP[start_unit]
  885. when '<tempC>'
  886. @scalar + 273.15
  887. when '<tempK>'
  888. @scalar
  889. when '<tempF>'
  890. (@scalar+459.67)*Rational(5,9)
  891. when '<tempR>'
  892. @scalar*Rational(5,9)
  893. end
  894. end
  895. q= case @@UNIT_MAP[target_unit]
  896. when '<tempC>'
  897. @base_scalar - 273.15
  898. when '<tempK>'
  899. @base_scalar
  900. when '<tempF>'
  901. @base_scalar * Rational(9,5) - 459.67
  902. when '<tempR>'
  903. @base_scalar * Rational(9,5)
  904. end
  905. return Unit.new("#{q} #{target_unit}")
  906. else
  907. case other
  908. when Unit
  909. return self if other.units == self.units
  910. target = other
  911. when String
  912. target = Unit.new(other)
  913. else
  914. raise ArgumentError, "Unknown target units"
  915. end
  916. raise ArgumentError, "Incompatible Units" unless self =~ target
  917. _numerator1 = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
  918. _denominator1 = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
  919. _numerator2 = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
  920. _denominator2 = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
  921. q = @scalar * ( (_numerator1 + _denominator2).inject(1) {|product,n| product*n} ) /
  922. ( (_numerator2 + _denominator1).inject(1) {|product,n| product*n} )
  923. return Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
  924. end
  925. end
  926. alias :>> :convert_to
  927. alias :to :convert_to
  928. # converts the unit back to a float if it is unitless. Otherwise raises an exception
  929. # @return [Float]
  930. # @raise [RuntimeError] when not unitless
  931. def to_f
  932. return @scalar.to_f if self.unitless?
  933. raise RuntimeError, "Cannot convert '#{self.to_s}' to Float unless unitless. Use Unit#scalar"
  934. end
  935. # converts the unit back to a complex if it is unitless. Otherwise raises an exception
  936. # @return [Complex]
  937. # @raise [RuntimeError] when not unitless
  938. def to_c
  939. return Complex(@scalar) if self.unitless?
  940. raise RuntimeError, "Cannot convert '#{self.to_s}' to Complex unless unitless. Use Unit#scalar"
  941. end
  942. # if unitless, returns an int, otherwise raises an error
  943. # @return [Integer]
  944. # @raise [RuntimeError] when not unitless
  945. def to_i
  946. return @scalar.to_int if self.unitless?
  947. raise RuntimeError, "Cannot convert '#{self.to_s}' to Integer unless unitless. Use Unit#scalar"
  948. end
  949. alias :to_int :to_i
  950. # if unitless, returns a Rational, otherwise raises an error
  951. # @return [Rational]
  952. # @raise [RuntimeError] when not unitless
  953. def to_r
  954. return @scalar.to_r if self.unitless?
  955. raise RuntimeError, "Cannot convert '#{self.to_s}' to Rational unless unitless. Use Unit#scalar"
  956. end
  957. # returns the 'unit' part of the Unit object without the scalar
  958. # @return [String]
  959. def units
  960. return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
  961. return @unit_name unless @unit_name.nil?
  962. output_numerator = []
  963. output_denominator = []
  964. num = @numerator.clone.compact
  965. den = @denominator.clone.compact
  966. if @numerator == UNITY_ARRAY
  967. output_numerator << "1"
  968. else
  969. while defn = Unit.definition(num.shift) do
  970. if defn && defn.prefix?
  971. output_numerator << defn.display_name + Unit.definition(num.shift).display_name
  972. else
  973. output_numerator << defn.display_name
  974. end
  975. end
  976. end
  977. if @denominator == UNITY_ARRAY
  978. output_denominator = []
  979. else
  980. while defn = Unit.definition(den.shift) do
  981. if defn && defn.prefix?
  982. output_denominator << defn.display_name + Unit.definition(den.shift).display_name
  983. else
  984. output_denominator << defn.display_name
  985. end
  986. end
  987. end
  988. on = output_numerator.uniq.
  989. map {|x| [x, output_numerator.count(x)]}.
  990. map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))}
  991. od = output_denominator.uniq.
  992. map {|x| [x, output_denominator.count(x)]}.
  993. map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))}
  994. out = "#{on.join('*')}#{od.empty? ? '': '/' + od.join('*')}".strip
  995. @unit_name = out unless self.kind == :temperature
  996. return out
  997. end
  998. # negates the scalar of the Unit
  999. # @return [Numeric,Unit]
  1000. def -@
  1001. return -@scalar if self.unitless?
  1002. return (self.dup * -1)
  1003. end
  1004. # absolute value of a unit
  1005. # @return [Numeric,Unit]
  1006. def abs
  1007. return @scalar.abs if self.unitless?
  1008. return Unit.new(@scalar.abs, @numerator, @denominator)
  1009. end
  1010. # ceil of a unit
  1011. # @return [Numeric,Unit]
  1012. def ceil
  1013. return @scalar.ceil if self.unitless?
  1014. return Unit.new(@scalar.ceil, @numerator, @denominator)
  1015. end
  1016. # @return [Numeric,Unit]
  1017. def floor
  1018. return @scalar.floor if self.unitless?
  1019. return Unit.new(@scalar.floor, @numerator, @denominator)
  1020. end
  1021. # @return [Numeric,Unit]
  1022. def round
  1023. return @scalar.round if self.unitless?
  1024. return Unit.new(@scalar.round, @numerator, @denominator)
  1025. end
  1026. # @return [Numeric, Unit]
  1027. def truncate
  1028. return @scalar.truncate if self.unitless?
  1029. return Unit.new(@scalar.truncate, @numerator, @denominator)
  1030. end
  1031. # returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
  1032. # only works when the scalar is an integer
  1033. # @return [Unit]
  1034. # @raise [ArgumentError] when scalar is not equal to an integer
  1035. def succ
  1036. raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
  1037. return Unit.new(@scalar.to_i.succ, @numerator, @denominator)
  1038. end
  1039. alias :next :succ
  1040. # returns previous unit in a range. '2 mm'.unit.pred #=> '1 mm'.unit
  1041. # only works when the scalar is an integer
  1042. # @return [Unit]
  1043. # @raise [ArgumentError] when scalar is not equal to an integer
  1044. def pred
  1045. raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
  1046. return Unit.new(@scalar.to_i.pred, @numerator, @denominator)
  1047. end
  1048. # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.
  1049. # @return [Time]
  1050. def to_time
  1051. return Time.at(self)
  1052. end
  1053. alias :time :to_time
  1054. # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date
  1055. # defined by DateTime
  1056. # @return [DateTime]
  1057. def to_datetime
  1058. return DateTime.new!(self.convert_to('d').scalar)
  1059. end
  1060. # @return [Date]
  1061. def to_date
  1062. return Date.new0(self.convert_to('d').scalar)
  1063. end
  1064. # true if scalar is zero
  1065. # @return [Boolean]
  1066. def zero?
  1067. return self.base_scalar.zero?
  1068. end
  1069. # @example '5 min'.unit.ago
  1070. # @return [Unit]
  1071. def ago
  1072. return self.before
  1073. end
  1074. # @example '5 min'.before(time)
  1075. # @return [Unit]
  1076. def before(time_point = ::Time.now)
  1077. case time_point
  1078. when Time, Date, DateTime
  1079. return (time_point - self rescue time_point.to_datetime - self)
  1080. else
  1081. raise ArgumentError, "Must specify a Time, Date, or DateTime"
  1082. end
  1083. end
  1084. alias :before_now :before
  1085. # @example 'min'.since(time)
  1086. # @param [Time, Date, DateTime] time_point
  1087. # @return [Unit]
  1088. # @raise [ArgumentError] when time point is not a Time, Date, or DateTime
  1089. def since(time_point)
  1090. case time_point
  1091. when Time
  1092. return (Time.now - time_point).unit('s').convert_to(self)
  1093. when DateTime, Date
  1094. return (DateTime.now - time_point).unit('d').convert_to(self)
  1095. else
  1096. raise ArgumentError, "Must specify a Time, Date, or DateTime"
  1097. end
  1098. end
  1099. # @example 'min'.until(time)
  1100. # @param [Time, Date, DateTime] time_point
  1101. # @return [Unit]
  1102. def until(time_point)
  1103. case time_point
  1104. when Time
  1105. return (time_point - Time.now).unit('s').convert_to(self)
  1106. when DateTime, Date
  1107. return (time_point - DateTime.now).unit('d').convert_to(self)
  1108. else
  1109. raise ArgumentError, "Must specify a Time, Date, or DateTime"
  1110. end
  1111. end
  1112. # @example '5 min'.from(time)
  1113. # @param [Time, Date, DateTime] time_point
  1114. # @return [Time, Date, DateTime]
  1115. # @raise [ArgumentError] when passed argument is not a Time, Date, or DateTime
  1116. def from(time_point)
  1117. case time_point
  1118. when Time, DateTime, Date
  1119. return (time_point + self rescue time_point.to_datetime + self)
  1120. else
  1121. raise ArgumentError, "Must specify a Time, Date, or DateTime"
  1122. end
  1123. end
  1124. alias :after :from
  1125. alias :from_now :from
  1126. # automatically coerce objects to units when possible
  1127. # if an object defines a 'to_unit' method, it will be coerced using that method
  1128. # @param [Object, #to_unit]
  1129. # @return [Array]
  1130. def coerce(other)
  1131. if other.respond_to? :to_unit
  1132. return [other.to_unit, self]
  1133. end
  1134. case other
  1135. when Unit
  1136. return [other, self]
  1137. else
  1138. return [Unit.new(other), self]
  1139. end
  1140. end
  1141. # Protected and Private Functions that should only be called from this class
  1142. protected
  1143. # figure out what the scalar part of the base unit for this unit is
  1144. # @return [nil]
  1145. def update_base_scalar
  1146. if self.is_base?
  1147. @base_scalar = @scalar
  1148. @signature = unit_signature
  1149. else
  1150. base = self.to_base
  1151. @base_scalar = base.scalar
  1152. @signature = base.signature
  1153. end
  1154. end
  1155. # calculates the unit signature vector used by unit_signature
  1156. # @return [Array]
  1157. # @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20
  1158. def unit_signature_vector
  1159. return self.to_base.unit_signature_vector unless self.is_base?
  1160. vector = Array.new(SIGNATURE_VECTOR.size,0)
  1161. # it's possible to have a kind that misses the array... kinds like :counting
  1162. # are more like prefixes, so don't use them to calculate the vector
  1163. @numerator.map {|element| Unit.definition(element)}.each do |definition|
  1164. index = SIGNATURE_VECTOR.index(definition.kind)
  1165. vector[index] += 1 if index
  1166. end
  1167. @denominator.map {|element| Unit.definition(element)}.each do |definition|
  1168. index = SIGNATURE_VECTOR.index(definition.kind)
  1169. vector[index] -= 1 if index
  1170. end
  1171. raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? {|x| x.abs >=20}
  1172. return vector
  1173. end
  1174. private
  1175. # used by #dup to duplicate a Unit
  1176. # @param [Unit] other
  1177. # @private
  1178. def initialize_copy(other)
  1179. @numerator = other.numerator.dup
  1180. @denominator = other.denominator.dup
  1181. end
  1182. # calculates the unit signature id for use in comparing compatible units and simplification
  1183. # the signature is based on a simple classification of units and is based on the following publication
  1184. #
  1185. # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661
  1186. # @see http://doi.ieeecomputersociety.org/10.1109/32.403789
  1187. # @return [Array]
  1188. def unit_signature
  1189. return @signature unless @signature.nil?
  1190. vector = unit_signature_vector
  1191. vector.each_with_index {|item,index| vector[index] = item * 20**index}
  1192. @signature=vector.inject(0) {|sum,n| sum+n}
  1193. return @signature
  1194. end
  1195. # @param [Numeric] q quantity
  1196. # @param [Array] n numerator
  1197. # @param [Array] d denominator
  1198. # @return [Hash]
  1199. def self.eliminate_terms(q, n, d)
  1200. num = n.dup
  1201. den = d.dup
  1202. num.delete_if {|v| v == UNITY}
  1203. den.delete_if {|v| v == UNITY}
  1204. combined = Hash.new(0)
  1205. i = 0
  1206. loop do
  1207. break if i > num.size
  1208. if @@PREFIX_VALUES.has_key? num[i]
  1209. k = [num[i],num[i+1]]
  1210. i += 2
  1211. else
  1212. k = num[i]
  1213. i += 1
  1214. end
  1215. combined[k] += 1 unless k.nil? || k == UNITY
  1216. end
  1217. j = 0
  1218. loop do
  1219. break if j > den.size
  1220. if @@PREFIX_VALUES.has_key? den[j]
  1221. k = [den[j],den[j+1]]
  1222. j += 2
  1223. else
  1224. k = den[j]
  1225. j += 1
  1226. end
  1227. combined[k] -= 1 unless k.nil? || k == UNITY
  1228. end
  1229. num = []
  1230. den = []
  1231. for key, value in combined do
  1232. case
  1233. when value > 0
  1234. value.times {num << key}
  1235. when value < 0
  1236. value.abs.times {den << key}
  1237. end
  1238. end
  1239. num = UNITY_ARRAY if num.empty?
  1240. den = UNITY_ARRAY if den.empty?
  1241. return {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
  1242. end
  1243. # parse a string into a unit object.
  1244. # Typical formats like :
  1245. # "5.6 kg*m/s^2"
  1246. # "5.6 kg*m*s^-2"
  1247. # "5.6 kilogram*meter*second^-2"
  1248. # "2.2 kPa"
  1249. # "37 degC"
  1250. # "1" -- creates a unitless constant with value 1
  1251. # "GPa" -- creates a unit with scalar 1 with units 'GPa'
  1252. # 6'4" -- recognized as 6 feet + 4 inches
  1253. # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
  1254. # @return [nil | Unit]
  1255. # @todo This should either be a separate class or at least a class method
  1256. def parse(passed_unit_string="0")
  1257. unit_string = passed_unit_string.dup
  1258. if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
  1259. unit_string = "#{$1} USD"
  1260. end
  1261. unit_string.gsub!(/%/,'percent')
  1262. unit_string.gsub!(/'/,'feet')
  1263. unit_string.gsub!(/"/,'inch')
  1264. unit_string.gsub!(/#/,'pound')
  1265. #:nocov:
  1266. #:nocov_19:
  1267. if defined?(Uncertain) && unit_string =~ /(\+\/-|&plusmn;)/
  1268. value, uncertainty, unit_s = unit_string.scan(UNCERTAIN_REGEX)[0]
  1269. result = unit_s.unit * Uncertain.new(value.to_f,uncertainty.to_f)
  1270. copy(result)
  1271. return
  1272. end
  1273. #:nocov:
  1274. #:nocov_19:
  1275. if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
  1276. real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
  1277. result = Unit(unit_s || '1') * Complex(real.to_f,imaginary.to_f)
  1278. copy(result)
  1279. return
  1280. end
  1281. if defined?(Rational) && unit_string =~ RATIONAL_NUMBER
  1282. numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
  1283. result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i)
  1284. copy(result)
  1285. return
  1286. end
  1287. unit_string =~ NUMBER_REGEX
  1288. unit = @@cached_units[$2]
  1289. mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0
  1290. mult = mult.to_int if (mult.to_int == mult)
  1291. if unit
  1292. copy(unit)
  1293. @scalar *= mult
  1294. @base_scalar *= mult
  1295. return self
  1296. end
  1297. unit_string.gsub!(/<(#{@@UNIT_REGEX})><(#{@@UNIT_REGEX})>/, '\1*\2')
  1298. unit_string.gsub!(/[<>]/,"")
  1299. if unit_string =~ /:/
  1300. hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
  1301. raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?}
  1302. result = "#{hours || 0} h".unit +
  1303. "#{minutes || 0} minutes".unit +
  1304. "#{seconds || 0} seconds".unit +
  1305. "#{microseconds || 0} usec".unit
  1306. copy(result)
  1307. return
  1308. end
  1309. # Special processing for unusual unit strings
  1310. # feet -- 6'5"
  1311. feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
  1312. if (feet && inches)
  1313. result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
  1314. copy(result)
  1315. return
  1316. end
  1317. # weight -- 8 lbs 12 oz
  1318. pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
  1319. if (pounds && oz)
  1320. result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
  1321. copy(result)
  1322. return
  1323. end
  1324. # more than one per. I.e., "1 m/s/s"
  1325. raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
  1326. raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s[02-9]/).size > 0
  1327. @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts
  1328. top.scan(TOP_REGEX).each do |item|
  1329. n = item[1].to_i
  1330. x = "#{item[0]} "
  1331. case
  1332. when n>=0
  1333. top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
  1334. when n<0
  1335. bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
  1336. end
  1337. end
  1338. bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom
  1339. @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty?
  1340. @scalar = 1 unless @scalar.kind_of? Numeric
  1341. @scalar = @scalar.to_int if (@scalar.to_int == @scalar)
  1342. @numerator ||= UNITY_ARRAY
  1343. @denominator ||= UNITY_ARRAY
  1344. @numerator = top.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if top
  1345. @denominator = bottom.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if bottom
  1346. # eliminate all known terms from this string. This is a quick check to see if the passed unit
  1347. # contains terms that are not defined.
  1348. used = "#{top} #{bottom}".to_s.gsub(Unit.unit_match_regex,'').gsub(/[\d\*, "'_^\/\$]/,'')
  1349. raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty?
  1350. @numerator = @numerator.map do |item|
  1351. @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP

Large files files are truncated, but you can click here to view the full file