require 'mathn' require 'rational' require 'date' require 'parsedate' # = Ruby Units 0.3.8 # # 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. VERSION = '0.3.8' @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@OUTPUT_MAP = {} @@BASE_UNITS = ['','','','', '', '','','','','','','','',''] UNITY = '<1>' UNITY_ARRAY= [UNITY] FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/ TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/ LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/ SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)} NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/ UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/ TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/ BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/ KELVIN = [''] FARENHEIT = [''] RANKINE = [''] CELCIUS = [''] SIGNATURE_VECTOR = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle, :capacitance] @@KINDS = { -312058=>:resistance, -312038=>:inductance, -152040=>:magnetism, -152038=>:magnetism, -152058=>:potential, -39=>:acceleration, -38=>:radiation, -20=>:frequency, -19=>:speed, -18=>:viscosity, 0=>:unitless, 1=>:length, 2=>:area, 3=>:volume, 20=>:time, 400=>:temperature, 7942=>:power, 7959=>:pressure, 7962=>:energy, 7979=>:viscosity, 7981=>:force, 7997=>:mass_concentration, 8000=>:mass, 159999=>:magnetism, 160000=>:current, 160020=>:charge, 312058=>:resistance, 3199980=>:activity, 3199997=>:molar_concentration, 3200000=>:substance, 63999998=>:illuminance, 64000000=>:luminous_power, 1280000000=>:currency, 25600000000=>:memory, 511999999980=>:angular_velocity, 512000000000=>:angle, 10240000000000=>:capacitance, } @@cached_units = {} @@base_unit_cache = {} def self.setup @@ALL_UNIT_DEFINITIONS = UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS) for unit in (@@ALL_UNIT_DEFINITIONS) do key, value = unit if value[2] == :prefix then @@PREFIX_VALUES[key]=value[1] for name in value[0] do @@PREFIX_MAP[name]=key end else @@UNIT_VALUES[key]={} @@UNIT_VALUES[key][:scalar]=value[1] @@UNIT_VALUES[key][:numerator]=value[3] if value[3] @@UNIT_VALUES[key][:denominator]=value[4] if value[4] for name in value[0] do @@UNIT_MAP[name]=key end end @@OUTPUT_MAP[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('|') @@UNIT_MATCH_REGEX = /(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/ Unit.new(1) end include Comparable attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar, :base_numerator, :base_denominator, :output, :unit_name def to_yaml_properties %w{@scalar @numerator @denominator @signature @base_scalar} end def copy(from) @scalar = from.scalar @numerator = from.numerator @denominator = from.denominator @is_base = from.is_base? @signature = from.signature @base_scalar = from.base_scalar @output = from.output rescue nil @unit_name = from.unit_name rescue nil end # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string # before YAML'izing it. def to_yaml( opts = {} ) YAML::quick_emit( object_id, opts ) do |out| out.map( taguri, to_yaml_style ) do |map| for m in to_yaml_properties do map.add( m[1..-1], instance_variable_get( m ) ) end end end end # 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 scalar 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) @scalar = nil @base_scalar = nil @unit_name = nil @signature = nil @output = nil if options.size == 2 begin cached = @@cached_units[options[1]] * options[0] copy(cached) rescue initialize("#{options[0]} #{options[1]}") end return end if options.size == 3 begin cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0] copy(cached) rescue initialize("#{options[0]} #{options[1]}/#{options[2]}") end return end case options[0] 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 = [''] @denominator = UNITY_ARRAY when DateTime: @scalar = options[0].ajd @numerator = [''] @denominator = UNITY_ARRAY when "": raise ArgumentError, "No Unit Specified" when String: parse(options[0]) else raise ArgumentError, "Invalid Unit Format" end self.update_base_scalar self.replace_temperature unary_unit = self.units || "" opt_units = options[0].scan(NUMBER_REGEX)[0][1] if String === options[0] unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(temp|deg(C|K|R|F))|(pounds|lbs[ ,]\d+ ounces|oz)|('\d+")|(ft|feet[ ,]\d+ in|inch|inches)|%|(#{TIME_REGEX})/) @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty? end unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /(temp|deg)(C|K|R|F)/) then @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit) end [@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze} self end def kind return @@KINDS[self.signature] end def self.cached return @@cached_units end def self.clear_cache @@cached_units = {} @@base_unit_cache = {} end def self.base_unit_cache return @@base_unit_cache end def to_unit self end alias :unit :to_unit # Returns 'true' if the Unit is represented in base units def is_base? return @is_base if defined? @is_base return @is_base=true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/ n = @numerator + @denominator for x in n.compact do return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x))) end return @is_base = true end # convert to base SI units # results of the conversion are cached so subsequent calls to this will be fast def to_base return self if self.is_base? cached = @@base_unit_cache[self.units] * self.scalar rescue nil return cached if cached num = [] den = [] q = 1 for unit in @numerator.compact do if @@PREFIX_VALUES[unit] q *= @@PREFIX_VALUES[unit] else q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit] num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator] den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator] end end for unit in @denominator.compact do if @@PREFIX_VALUES[unit] q /= @@PREFIX_VALUES[unit] else q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit] den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator] num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator] end end num = num.flatten.compact den = den.flatten.compact num = UNITY_ARRAY if num.empty? base= Unit.new(Unit.eliminate_terms(q,num,den)) @@base_unit_cache[self.units]=base return base * @scalar end # Generate human readable output. # If the name of a unit is passed, the unit will first be converted to the target unit before output. # some named conversions are available # # :ft - outputs in feet and inches (e.g., 6'4") # :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz) # # You can also pass a standard format string (i.e., '%0.2f') # or a strftime format string. # # output is cached so subsequent calls for the same format will be fast # def to_s(target_units=nil) out = @output[target_units] rescue nil if out return out else case target_units when :ft: inches = self.to("in").scalar.to_int out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" when :lbs: ounces = self.to("oz").scalar.to_int out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" when String begin #first try a standard format string target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/ out = $2 ? self.to($2).to_s($1) : "#{($1 || '%g') % @scalar || 0} #{self.units}".strip rescue #if that is malformed, try a time string out = (Time.gm(0) + self).strftime(target_units) end else out = "#{'%g' % @scalar} #{self.units}".strip end @output = {target_units => out} return out end end # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump' def inspect(option=nil) return super() if option == :dump self.to_s end # returns true if no associated units # false, even if the units are "unitless" like 'radians, each, etc' def unitless? (@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY) 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) case other when Unit: raise ArgumentError, "Incompatible Units" unless self =~ other self.base_scalar <=> other.base_scalar else x,y = coerce(other) x <=> y end end # check to see if units are compatible, but not the scalar 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.units =~ /regexp/ def =~(other) case other when Unit : self.signature == other.signature else x,y = coerce(other) x =~ y end end alias :compatible? :=~ alias :compatible_with? :=~ # 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.scalar == other.scalar) && (self.units == other.units) else x,y = coerce(other) x === y end end alias :same? :=== alias :same_as? :=== # 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 def +(other) if Unit === other if self =~ other then @q ||= @@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) else raise ArgumentError, "Incompatible Units" end elsif Time === other other + self else x,y = coerce(other) y + x end end # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately # throws an exception if the units are not compatible. def -(other) if Unit === other if self =~ other then @q ||= @@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature) else raise ArgumentError, "Incompatible Units" end elsif Time === other other - self else x,y = coerce(other) y-x end end # Multiply two units. def *(other) case other when Unit opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator) opts.merge!(:signature => @signature + other.signature) Unit.new(opts) when Numeric Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) else x,y = coerce(other) x * y end end # Divide two units. # Throws an exception if divisor is 0 def /(other) case other when Unit raise ZeroDivisionError if other.zero? opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator) opts.merge!(:signature=> @signature - other.signature) Unit.new(opts) when Numeric raise ZeroDivisionError if other.zero? Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) 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 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 def **(other) if Numeric === other return Unit("1") if other.zero? return self if other == 1 return self.inverse if other == -1 end case other when Rational: self.power(other.numerator).root(other.denominator) when Integer: self.power(other) when Float: return self**(other.to_i) if other == other.to_i valid = (1..9).map {|x| 1/x} raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs self.root((1/other).to_int) else raise ArgumentError, "Invalid Exponent" end end # returns the unit raised to the n-th power. Integers only def power(n) raise ArgumentError, "Can only use Integer exponenents" unless Integer === n return self if n == 1 return Unit("1") if n == 0 return self.inverse if n == -1 if n > 0 then (1..n.to_i).inject(Unit.new("1")) {|product, x| product * self} else (1..-n.to_i).inject(Unit.new("1")) {|product, x| product / self} end end # Calculates the n-th root of a unit, where n = (1..9) # if n < 0, returns 1/unit^(1/n) def root(n) raise ArgumentError, "Exponent must an Integer" unless Integer === n raise ArgumentError, "0th root undefined" if n == 0 return self if n == 1 return self.root(n.abs).inverse if n < 0 vec = self.unit_signature_vector vec=vec.map {|x| x % n} raise ArgumentError, "Illegal root" unless vec.max == 0 num = @numerator.dup den = @denominator.dup for item in @numerator.uniq do x = num.find_all {|i| i==item}.size r = ((x/n)*(n-1)).to_int r.times {|x| num.delete_at(num.index(item))} end for item in @denominator.uniq do x = den.find_all {|i| i==item}.size r = ((x/n)*(n-1)).to_int r.times {|x| den.delete_at(den.index(item))} end q = @scalar**(1/n) Unit.new(:scalar=>q,:numerator=>num,:denominator=>den) end # returns inverse of Unit (1/unit) def inverse Unit("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 to(other) return self if other.nil? return self if TrueClass === other return self if FalseClass === other if (Unit === other && other.units =~ /temp(K|C|R|F)/) || (String === other && other =~ /temp(K|C|R|F)/) raise ArgumentError, "Receiver is not a temperature unit" unless self.signature==400 start_unit = self.units target_unit = other.units rescue other q=case start_unit when 'degC': case target_unit when 'tempC' : @scalar when 'tempK' : @scalar + 273.15 when 'tempF' : @scalar * (9.0/5.0) + 32.0 when 'tempR' : @scalar * (9.0/5.0) + 491.67 end when 'degK': case target_unit when 'tempC' : @scalar - 273.15 when 'tempK' : @scalar when 'tempF' : @scalar * (9.0/5.0) - 459.67 when 'tempR' : @scalar * (9.0/5.0) end when 'degF': case target_unit when 'tempC' : (@scalar-32)*(5.0/9.0) when 'tempK' : (@scalar+459.67)*(5.0/9.0) when 'tempF' : @scalar when 'tempR' : @scalar + 459.67 end when 'degR': case target_unit when 'tempC' : @scalar*(5.0/9.0) -273.15 when 'tempK' : @scalar*(5.0/9.0) when 'tempF' : @scalar - 459.67 when 'tempR' : @scalar end else return self.to_base.to(other) unless self.is_base? #raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}" end target_unit =~ /temp(C|K|F|R)/ Unit.new("#{q} deg#{$1}") else case other when Unit: return self if other.units == self.units target = other when String: target = Unit.new(other) else raise ArgumentError, "Unknown target units" end raise ArgumentError, "Incompatible Units" unless self =~ target one = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact two = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} one = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact two = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} q = @scalar * v/y Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature) end end alias :>> :to alias :convert_to :to # converts the unit back to a float if it is unitless. Otherwise raises an exception def to_f return @scalar.to_f if self.unitless? raise RuntimeError, "Can't convert to float unless unitless. Use Unit#scalar" end # returns the 'unit' part of the Unit object without the scalar def units return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY return @unit_name unless @unit_name.nil? output_n = [] output_d =[] num = @numerator.clone.compact den = @denominator.clone.compact if @numerator == UNITY_ARRAY output_n << "1" else num.each_with_index do |token,index| if token && @@PREFIX_VALUES[token] then output_n << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[num[index+1]]}" num[index+1]=nil else output_n << "#{@@OUTPUT_MAP[token]}" if token end end end if @denominator == UNITY_ARRAY output_d = ['1'] else den.each_with_index do |token,index| if token && @@PREFIX_VALUES[token] then output_d << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[den[index+1]]}" den[index+1]=nil else output_d << "#{@@OUTPUT_MAP[token]}" if token end end 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]}" : ''))} out = "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip @unit_name = out unless self.kind == :temperature return out end # negates the scalar of the Unit def -@ return -@scalar if self.unitless? Unit.new(-@scalar,@numerator,@denominator) end # returns abs of scalar, without the units def abs return @scalar.abs end def ceil return @scalar.ceil if self.unitless? Unit.new(@scalar.ceil, @numerator, @denominator) end def floor return @scalar.floor if self.unitless? Unit.new(@scalar.floor, @numerator, @denominator) end # if unitless, returns an int, otherwise raises an error def to_int return @scalar.to_int if self.unitless? raise RuntimeError, 'Cannot convert to Integer, use Unit#scalar' end # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch. def to_time Time.at(self) end alias :time :to_time alias :to_i :to_int def truncate return @scalar.truncate if self.unitless? Unit.new(@scalar.truncate, @numerator, @denominator) end # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date # defined by DateTime def to_datetime DateTime.new(self.to('d').scalar) end def round return @scalar.round if self.unitless? Unit.new(@scalar.round, @numerator, @denominator) end # true if scalar is zero def zero? return @scalar.zero? end # '5 min'.unit.ago def ago self.before end # '5 min'.before(time) def before(time_point = ::Time.now) raise ArgumentError, "Must specify a Time" unless time_point if String === time_point time_point.time - self rescue time_point.datetime - self else time_point - self rescue time_point.to_datetime - self end end alias :before_now :before # 'min'.since(time) def since(time_point = ::Time.now) case time_point when Time: (Time.now - time_point).unit('s').to(self) when DateTime, Date: (DateTime.now - time_point).unit('d').to(self) when String: (DateTime.now - time_point.time(:context=>:past)).unit('d').to(self) else raise ArgumentError, "Must specify a Time, DateTime, or String" end end # 'min'.until(time) def until(time_point = ::Time.now) case time_point when Time: (time_point - Time.now).unit('s').to(self) when DateTime, Date: (time_point - DateTime.now).unit('d').to(self) when String: r = (time_point.time(:context=>:future) - DateTime.now) Time === time_point.time ? r.unit('s').to(self) : r.unit('d').to(self) else raise ArgumentError, "Must specify a Time, DateTime, or String" end end # '5 min'.from(time) def from(time_point = ::Time.now) raise ArgumentError, "Must specify a Time" unless time_point if String === time_point time_point.time + self rescue time_point.datetime + self else time_point + self rescue time_point.to_datetime + self end end alias :after :from alias :from_now :from # returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit # only works when the scalar is an integer def succ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i q = @scalar.to_i.succ Unit.new(q, @numerator, @denominator) end # automatically coerce objects to units when possible # if an object defines a 'to_unit' method, it will be coerced using that method def coerce(other) if other.respond_to? :to_unit return [other.to_unit, self] end case other when Unit : [other, self] else [Unit.new(other), self] end end # Protected and Private Functions that should only be called from this class protected def update_base_scalar return @base_scalar unless @base_scalar.nil? if self.is_base? @base_scalar = @scalar @signature = unit_signature else base = self.to_base @base_scalar = base.scalar @signature = base.signature end end # calculates the unit signature vector used by unit_signature def unit_signature_vector return self.to_base.unit_signature_vector unless self.is_base? result = self vector = Array.new(SIGNATURE_VECTOR.size,0) for element in @numerator if r=@@ALL_UNIT_DEFINITIONS[element] n = SIGNATURE_VECTOR.index(r[2]) vector[n] = vector[n] + 1 if n end end for element in @denominator if r=@@ALL_UNIT_DEFINITIONS[element] n = SIGNATURE_VECTOR.index(r[2]) vector[n] = vector[n] - 1 if n end end vector end def replace_temperature return self unless self.kind == :temperature && self.units =~ /temp(R|K|F|C)/ un = $1 @numerator = case un when 'R' : RANKINE when 'C' : CELCIUS when 'F' : FARENHEIT when 'K' : KELVIN end @unit_name = nil r= self.to("tempK") copy(r) end private def initialize_copy(other) @numerator = other.numerator.dup @denominator = other.denominator.dup 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 return @signature unless @signature.nil? vector = unit_signature_vector vector.each_with_index {|item,index| vector[index] = item * 20**index} @signature=vector.inject(0) {|sum,n| sum+n} end def self.eliminate_terms(q, n, d) num = n.dup den = d.dup num.delete_if {|v| v == UNITY} den.delete_if {|v| v == UNITY} combined = Hash.new(0) i = 0 loop do break if i > num.size if @@PREFIX_VALUES.has_key? num[i] k = [num[i],num[i+1]] i += 2 else k = num[i] i += 1 end combined[k] += 1 unless k.nil? || k == UNITY end j = 0 loop do break if j > den.size if @@PREFIX_VALUES.has_key? den[j] k = [den[j],den[j+1]] j += 2 else k = den[j] j += 1 end combined[k] -= 1 unless k.nil? || k == UNITY end num = [] den = [] for key, value in combined do case when value > 0 : value.times {num << key} when value < 0 : value.abs.times {den << key} end end num = UNITY_ARRAY if num.empty? den = UNITY_ARRAY if den.empty? {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact} end # parse a string into a unit object. # Typical formats like : # "5.6 kg*m/s^2" # "5.6 kg*m*s^-2" # "5.6 kilogram*meter*second^-2" # "2.2 kPa" # "37 degC" # "1" -- creates a unitless constant with value 1 # "GPa" -- creates a unit with scalar 1 with units 'GPa' # 6'4" -- recognized as 6 feet + 4 inches # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces def parse(passed_unit_string="0") unit_string = passed_unit_string.dup if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ unit_string = "#{$1} USD" end unit_string.gsub!(/%/,'percent') unit_string.gsub!(/'/,'feet') unit_string.gsub!(/"/,'inch') unit_string.gsub!(/#/,'pound') unit_string =~ NUMBER_REGEX unit = @@cached_units[$2] mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0 if unit copy(unit) @scalar *= mult @base_scalar *= mult return self end unit_string.gsub!(/[<>]/,"") if unit_string =~ /:/ hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0] raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?} result = "#{hours || 0} h".unit + "#{minutes || 0} minutes".unit + "#{seconds || 0} seconds".unit + "#{microseconds || 0} usec".unit copy(result) return end # Special processing for unusual unit strings # feet -- 6'5" feet, inches = unit_string.scan(FEET_INCH_REGEX)[0] if (feet && inches) result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches") copy(result) return end # weight -- 8 lbs 12 oz pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0] if (pounds && oz) result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz") copy(result) return end raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1 raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s\d+\S*/).size > 0 @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts top.scan(TOP_REGEX).each do |item| n = item[1].to_i x = "#{item[0]} " case when n>=0 : top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n} when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"") end end bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty? @scalar = 1 unless @scalar.kind_of? Numeric @numerator ||= UNITY_ARRAY @denominator ||= UNITY_ARRAY @numerator = top.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if top @denominator = bottom.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if bottom us = "#{(top || '' + bottom || '')}".to_s.gsub(@@UNIT_MATCH_REGEX,'').gsub(/[\d\*, "'_^\/\$]/,'') raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless us.empty? @numerator = @numerator.map do |item| @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]] end.flatten.compact.delete_if {|x| x.empty?} @denominator = @denominator.map do |item| @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]] end.flatten.compact.delete_if {|x| x.empty?} @numerator = UNITY_ARRAY if @numerator.empty? @denominator = UNITY_ARRAY if @denominator.empty? self end end # Allow date objects to do offsets by a time unit # Date.today + U"1 week" => gives today+1 week class Date alias :unit_date_add :+ def +(unit) case unit when Unit: unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units unit_date_add(unit.to('day').scalar) when Time: unit_date_add(unit.to_datetime) else unit_date_add(unit) end end alias :unit_date_sub :- def -(unit) case unit when Unit: unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units unit_date_sub(unit.to('day').scalar) when Time: unit_date_sub(unit.to_datetime) else unit_date_sub(unit) end end def to_unit(other = nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit def to_time Time.local(*ParseDate.parsedate(self.to_s)) end alias :units_datetime_inspect :inspect def inspect(raw = false) return self.units_datetime_inspect if raw self.to_s end def to_date Date.civil(self.year, self.month, self.day) end end class Object def Unit(*other) other.to_unit end alias :U :Unit alias :u :Unit end # make a unitless unit with a given scalar class Numeric def to_unit(other = nil) other ? Unit.new(self, other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit end # make a unit from an array # [1, 'mm'].unit => 1 mm class Array def to_unit(other = nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit end # make a string into a unit class String def to_unit(other = nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit alias :unit_format :% # format unit output using formating codes '%0.2f' % '1 mm'.unit => '1.00 mm' def %(*args) case args[0] when Unit: args[0].to_s(self) when Complex: args[0].to_s else unit_format(*args) end end #needed for compatibility with Rails, which defines a String.from method if self.public_instance_methods.include? 'from' alias :old_from :from end def from(time_point = ::Time.now) return old_from(time_point) if Integer === time_point self.unit.from(time_point) end alias :after :from alias :from_now :from def ago self.unit.ago end def before(time_point = ::Time.now) self.unit.before(time_point) end alias :before_now :before def since(time_point = ::Time.now) self.unit.since(time_point) end def until(time_point = ::Time.now) self.unit.until(time_point) end def to(other) self.unit.to(other) end def time(options = {}) self.to_time(options) rescue self.to_datetime(options) end def to_time(options = {}) begin #raises exception when Chronic not defined or when it returns a nil (i.e., can't parse the input) r = Chronic.parse(self,options) raise(ArgumentError, 'Invalid Time String') unless r return r rescue Time.local(*ParseDate.parsedate(self)) end end def to_datetime(options = {}) begin # raises an exception if Chronic.parse = nil or if Chronic not defined r = Chronic.parse(self,options).to_datetime rescue r=DateTime.civil(*ParseDate.parsedate(self)[0..5].compact) end raise RuntimeError, "Invalid Time String" if r == DateTime.new return r end def to_date(options={}) begin r = Chronic.parse(self,options).to_date rescue r = Date.civil(*ParseDate.parsedate(self)[0..5].compact) end raise RuntimeError, 'Invalid Date String' if r == Date.new return r end def datetime(options = {}) self.to_datetime(options) rescue self.to_time(options) end end # Allow time objects to use class Time class << self alias unit_time_at at end def self.at(*args) if Unit === args[0] unit_time_at(args[0].to("s").scalar) else unit_time_at(*args) end end def to_unit(other = nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit alias :unit_add :+ def to_datetime DateTime.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400 end def to_date Date.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400 end def +(other) case other when Unit: other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units begin unit_add(other.to('s').scalar) rescue RangeError self.to_datetime + other end when DateTime: unit_add(other.to_time) else unit_add(other) end end # usage: Time.in '5 min' def self.in(duration) Time.now + duration.to_unit end alias :unit_sub :- def -(other) case other when Unit: other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units begin unit_sub(other.to('s').scalar) rescue RangeError self.to_datetime - other end when DateTime: unit_sub(other.to_time) else unit_sub(other) end end end module Math alias unit_sin sin def sin(n) if Unit === n unit_sin(n.to('radian').scalar) else unit_sin(n) end end alias unit_cos cos def cos(n) if Unit === n unit_cos(n.to('radian').scalar) else unit_cos(n) end end alias unit_sinh sinh def sinh(n) if Unit === n unit_sinh(n.to('radian').scalar) else unit_sinh(n) end end alias unit_cosh cosh def cosh(n) if Unit === n unit_cosh(n.to('radian').scalar) else unit_cosh(n) end end alias unit_tan tan def tan(n) if Unit === n unit_tan(n.to('radian').scalar) else unit_tan(n) end end alias unit_tanh tanh def tanh(n) if Unit === n unit_tanh(n.to('radian').scalar) else unit_tanh(n) end end module_function :unit_sin module_function :sin module_function :unit_cos module_function :cos module_function :unit_sinh module_function :sinh module_function :unit_cosh module_function :cosh module_function :unit_tan module_function :tan module_function :unit_tanh module_function :tanh end Unit.setup