# = Ruby Units 0.1.1 # # Copyright 2006 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ # # http://www.sciwerks.org # # mailto://kevin.olbrich+ruby-units@gmail.com # # See README for detailed usage instructions and examples # # ==Unit Definition Format # # '' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{ } , %w{ } ], # # Prefixes (e.g., a :prefix classification) get special handling # Note: The accuracy of unit conversions depends on the precision of the conversion factor. # If you have more accurate estimates for particular conversion factors, please send them # to me and I will incorporate them into the next release. It is also incumbent on the end-user # to ensure that the accuracy of any conversions is sufficient for their intended application. # # While there are a large number of unit specified in the base package, # there are also a large number of units that are not included. # This package covers nearly all SI, Imperial, and units commonly used # in the United States. If your favorite units are not listed here, send me an email # # To add / override a unit definition, add a code block like this.. # # class Unit < Numeric # UNIT_DEFINITIONS = { # ' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{ } , %w{ } ] # } # end # Unit.setup class Unit < Numeric require 'units' # pre-generate hashes from unit definitions for performance. @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@OUTPUT_MAP = {} @@UNIT_VECTORS = {} def self.setup (UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value| if value[2] == :prefix then @@PREFIX_VALUES[Regexp.escape(key)]=value[1] value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key} else @@UNIT_VALUES[Regexp.escape(key)]={} @@UNIT_VALUES[Regexp.escape(key)][:quantity]=value[1] @@UNIT_VALUES[Regexp.escape(key)][:numerator]=value[3] if value[3] @@UNIT_VALUES[Regexp.escape(key)][:denominator]=value[4] if value[4] value[0].each {|x| @@UNIT_MAP[Regexp.escape(x)]=key} @@UNIT_VECTORS[value[2]] = [] unless @@UNIT_VECTORS[value[2]] @@UNIT_VECTORS[value[2]] = @@UNIT_VECTORS[value[2]]+[Regexp.escape(key)] end @@OUTPUT_MAP[Regexp.escape(key)]=value[0][0] end @@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| prefix.length}.reverse.join('|') @@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| unit.length}.reverse.join('|') end self.setup include Comparable attr_reader :quantity, :numerator, :denominator, :signature, :base_quantity # Create a new Unit object. Can be initialized using a string, or a hash # Valid formats 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 quantity 1 with units 'GPa' # 6'4" -- recognized as 6 feet + 4 inches # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces # def initialize(options) case options when String: parse(options) when Hash: @quantity = options[:quantity] || 1 @numerator = options[:numerator] || ["<1>"] @denominator = options[:denominator] || [] when Array: parse("#{options[0]} #{options[1]}/#{options[2]}") when Numeric: parse(options.to_s) else raise ArgumentError, "Invalid Unit Format" end self.update_base_quantity self.unit_signature self.freeze end def initialize_copy(other) @numerator = other.numerator.clone @denominator = other.denominator.clone end # Returns 'true' if the Unit is represented in base units def is_base? n = @numerator + @denominator n.each do |x| return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x))) end return true end #convert to base SI units def to_base return self if self.is_base? num = [] den = [] q = @quantity.to_f @numerator.each do |unit| if @@PREFIX_VALUES[Regexp.escape(unit)] q *= @@PREFIX_VALUES[Regexp.escape(unit)] else q *= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)] num << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator] den << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator] end end @denominator.each do |unit| if @@PREFIX_VALUES[Regexp.escape(unit)] q /= @@PREFIX_VALUES[Regexp.escape(unit)] else q /= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)] den << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator] num << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator] end end num = num.flatten.compact den = den.flatten.compact num = ['<1>'] if num.empty? Unit.new(Unit.eliminate_terms(q,num,den)) end # Generate human readable output. # If the name of a unit is passed, the quantity will first be converted to the target unit before output. # some named conversions are available # # :ft - outputs in feet and inches (e.g., 6'4") # :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz) # def to_s(target_units=nil) case target_units when :ft: inches = (self >> "in").to_f "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" when :lbs: ounces = (self >> "oz").to_f "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" else target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/ format_string = "#{$1}" if $1 units = $2 return (self >> units).to_s(format_string) if units "#{(format_string || '%g') % @quantity} #{self.to_unit}".strip end end # Compare two Unit objects. Throws an exception if they are not of compatible types. # Comparisons are done based on the value of the unit in base SI units. def <=>(other) raise ArgumentError, "Incompatible Units" unless self =~ other return self.base_quantity <=> other.base_quantity end # check to see if units are compatible, but not the quantity part # this check is done by comparing signatures for performance reasons # if passed a string, it will create a unit object with the string and then do the comparison # this permits a syntax like: # unit =~ "mm" # if you want to do a regexp on the unit string do this ... # unit.to_unit =~ /regexp/ def =~(other) case other when Unit : self.signature == other.signature else x,y = coerce(other) x =~ y end end # Compare two units. Returns true if quantities and units match # # Unit("100 cm") === Unit("100 cm") # => true # Unit("100 cm") === Unit("1 m") # => false def ===(other) case other when Unit: (self.quantity == other.quantity) && (self.to_unit == other.to_unit) else x,y = coerce(other) x === y end end # Add two units together. Result is same units as receiver and quantity and base_quantity are updated appropriately # throws an exception if the units are not compatible. def +(other) if Unit === other if self =~ other then q = @quantity + (other >> self).quantity Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) else raise ArgumentError, "Incompatible Units" end else x,y = coerce(other) x + y end end # Subtract two units. Result is same units as receiver and quantity and base_quantity are updated appropriately # throws an exception if the units are not compatible. def -(other) if Unit === other if self =~ other then q = @quantity - (other >> self).quantity Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) else raise ArgumentError, "Incompatible Units" end else x,y = coerce(other) x - y end end # Multiply two units. # Throws an exception if multiplier is not a Unit or Numeric def *(other) if Unit === other Unit.new(Unit.eliminate_terms(@quantity*other.quantity, @numerator + other.numerator ,@denominator + other.denominator)) else x,y = coerce(other) x * y end end # Divide two units. # Throws an exception if divisor is not a Unit or Numeric def /(other) if Unit === other Unit.new(Unit.eliminate_terms(@quantity/other.quantity, @numerator + other.denominator ,@denominator + other.numerator)) else x,y = coerce(other) y / x end end # Exponentiate. Only takes integer powers. # Note that anything raised to the power of 0 results in a Unit object with a quantity of 1, and no units. # Throws an exception if exponent is not an integer. def **(other) raise ArgumentError, "Exponent must be Integer" unless Integer === other case when other.to_i > 0 : (1..other.to_i).inject(Unit.new("1")) {|product, n| product * self} when other.to_i == 0 : Unit.new("1") when other.to_i < 0 : (1..-other.to_i).inject(Unit.new("1")) {|product, n| product / self} end end # returns inverse of Unit (1/unit) def inverse (Unit.new("1") / self) end # convert to a specified unit string or to the same units as another Unit # # unit >> "kg" will covert to kilograms # unit1 >> unit2 converts to same units as unit2 object # # To convert a Unit object to match another Unit object, use: # unit1 >>= unit2 # Throws an exception if the requested target units are incompatible with current Unit. # # 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, Celcius, Farenheit, and Rankine scales. # # Note that if temperature is part of a compound unit, the temperature will be treated as a differential # and the units will be scaled appropriately. def >>(other) case other.class.to_s when 'Unit': target = other when 'String': target = Unit.new(other) else raise ArgumentError, "Unknown target units" end raise ArgumentError, "Incompatible Units" unless self =~ target if target.signature == 400 then # special handling for temperature conversions q=case self.numerator[0] when '': case target.numerator[0] when '' : @quantity when '' : @quantity + 273.15 when '': @quantity * (9.0/5.0) + 32.0 when '' : @quantity * (9.0/5.0) + 491.67 else raise ArgumentError, "Unknown temperature conversion requested" end when '': case target.numerator[0] when '' : @quantity - 273.15 when '' : @quantity when '': @quantity * (9.0/5.0) - 459.67 when '' : @quantity * (9.0/5.0) else raise ArgumentError, "Unknown temperature conversion requested" end when '': case target.numerator[0] when '' : (@quantity-32)*(5.0/9.0) when '' : (@quantity+459.67)*(5.0/9.0) when '': @quantity when '' : @quantity + 459.67 else raise ArgumentError, "Unknown temperature conversion requested" end when '': case target.numerator[0] when '' : @quantity*(5.0/9.0) -273.15 when '' : @quantity*(5.0/9.0) when '': @quantity - 459.67 when '' : @quantity else raise ArgumentError, "Unknown temperature conversion requested" end else raise ArgumentError, "Unknown temperature conversion requested" end Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator) else one = @numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact two = @denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} one = target.numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact two = target.denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} q = @quantity * v/y Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator) end end # calculates the unit signature vector used by unit_signature def unit_signature_vector result = self.to_base y = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle] vector = Array.new(y.size,0) y.each_with_index do |units,index| vector[index] = result.numerator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size vector[index] -= result.denominator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size end vector end # calculates the unit signature id for use in comparing compatible units and simplification # the signature is based on a simple classification of units and is based on the following publication # # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, # 21(8), Aug 1995, pp.651-661 # doi://10.1109/32.403789 # http://ieeexplore.ieee.org/Xplore/login.jsp?url=/iel1/32/9079/00403789.pdf?isnumber=9079&prod=JNL&arnumber=403789&arSt=651&ared=661&arAuthor=Novak%2C+G.S.%2C+Jr. # def unit_signature vector = unit_signature_vector vector.each_with_index {|item,index| vector[index] = item * 20**index} @signature=vector.inject(0) {|sum,n| sum+n} end # Eliminates terms in the passed numerator and denominator. Expands out prefixes and applies them to the # quantity. Returns a hash that can be used to initialize a new Unit object. def self.eliminate_terms(q, n, d) num = n.clone den = d.clone # cancel terms in both numerator and denominator num.each_with_index do |item,index| if i=den.index(item) num.delete_at(index) den.delete_at(i) end end num = num.flatten.compact den = den.flatten.compact # substitute in SI prefix multipliers and numerical constants num.each_with_index do |item, index| if item =~ /<([\dEe+-.]+)>/ q *= $1.to_f num.delete_at(index) elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)] q *= multiplier num.delete_at(index) end end den.each_with_index do |item, index| if item =~ /<([\dEe+-.]+)>/ q /= $1.to_f den.delete_at(index) elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)] q /= multiplier den.delete_at(index) end end num = ["<1>"] if num.empty? den = ["<1>"] if den.empty? {:quantity=>q, :numerator=>num, :denominator=>den} end # returns the quantity part of the Unit def to_f @quantity end # returns the 'unit' part of the Unit object without the quantity def to_unit return "" if @numerator == ["<1>"] && @denominator == ["<1>"] output_n = [] num = @numerator.clone den = @denominator.clone if @numerator == ["<1>"] output_n << "1" else num.each_with_index do |token,index| if token && @@PREFIX_VALUES[Regexp.escape(token)] then output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(@numerator[index+1])]}" num[index+1]=nil else output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token end end end output_d = den.map do |token| @@PREFIX_MAP[Regexp.escape(token)] ? @@OUTPUT_MAP[Regexp.escape(token)] : "#{@@OUTPUT_MAP[Regexp.escape(token)]} " end on = output_n.reject {|x| x.empty?}.map {|x| [x, output_n.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))} od = output_d.reject {|x| x.empty?}.map {|x| [x, output_d.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))} "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip end # negates the quantity of the Unit def -@ q = -self.quantity Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) end # returns abs of quantity, without the units def abs return @quantity.abs end def ceil q = self.quantity.ceil Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) end def floor q = self.quantity.floor Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) end def to_int q = self.quantity.to_int Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) end alias :to_i :to_int alias :truncate :to_int def round q = self.quantity.round Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) end # true if quantity is zero def zero? return @quantity.zero? end # returns self if zero? is false, nil otherwise #def nonzero? # self.zero? ? nil : self #end def update_base_quantity @base_quantity = self.is_base? ? @quantity : self.to_base.quantity self end def coerce(other) case other when Unit : [other, self] when String : [Unit.new(other), self] when Array: [Unit.new(other.join('*')), self] when Numeric : [Unit.new(other.to_s), self] else raise ArgumentError, "Invalid Unit Definition" end end private # parse a string into a unit object. # Typical formats like : # "5.6 kg*m/s^2" # "5.6 kg*m*s^-2" # "5.6 kilogram*meter*second^-2" # "2.2 kPa" # "37 degC" # "1" -- creates a unitless constant with value 1 # "GPa" -- creates a unit with quantity 1 with units 'GPa' # 6'4" -- recognized as 6 feet + 4 inches # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces def parse(unit_string="0") @numerator = ['<1>'] @denominator = ['<1>'] # Special processing for unusual unit strings # feet -- 6'5" feet, inches = unit_string.scan(/(\d+)[\s*|'|ft|feet\s*](\d+)[\s*|"|in|inches]/)[0] if (feet && inches) result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches") @quantity = result.quantity @numerator = result.numerator @denominator = result.denominator @base_quantity = result.base_quantity return self end # weight -- 8 lbs 12 oz pounds, oz = unit_string.scan(/(\d+)[\s|#|lbs|pounds|,]+(\d+)[\s*|oz|ounces]/)[0] if (pounds && oz) result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz") @quantity = result.quantity @numerator = result.numerator @denominator = result.denominator @base_quantity = result.base_quantity return self end @quantity, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts top.scan(/([^ \*]+)\^([\d-]+)/).each do |item| n = item[1].to_i x = "#{item[0]} " case when n>=0 : top.gsub!(/([^ \*]+)\^(\d+)/) {|s| x * n} when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!("#{item[0]}^#{item[1]}","") end end bottom.gsub!(/([^* ]+)\^(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom if @quantity.empty? if top =~ /[\dEe+.-]+/ @quantity = top.to_f # need this for 'number only' initialization else @quantity = 1 # need this for 'unit only' intialization end else @quantity = @quantity.to_f end @numerator = top.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if top @denominator = bottom.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if bottom @numerator = @numerator.map do |item| item.map {|x| Regexp.escape(x) if x} @@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]] end.flatten.compact.delete_if {|x| x.empty?} @denominator = @denominator.map do |item| item.map {|x| Regexp.escape(x) if x} @@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]] end.flatten.compact.delete_if {|x| x.empty?} @numerator = ['<1>'] if @numerator.empty? @denominator = ['<1>'] if @denominator.empty? self end end class Object def Unit(other) other.to_unit end end class Numeric def to_unit(other = nil) other ? Unit.new(self.to_s) >> other : Unit.new(self.to_s) end alias :unit :to_unit end class Array def to_unit(other = nil) other ? Unit.new("#{self[0]} #{self[1]}/#{self[2]}") >> other : Unit.new("#{self[0]} #{self[1]}/#{self[2]}") end alias :unit :to_unit end class String def to_unit(other = nil) other ? Unit.new(self) >> other : Unit.new(self) end alias :unit :to_unit end