lib/ruby-units.rb in ruby-units-0.2.3 vs lib/ruby-units.rb in ruby-units-0.3.1

- old
+ new

@@ -1,10 +1,10 @@ require 'mathn' require 'rational' require 'date' require 'parsedate' -# = Ruby Units 0.2.3 +# = Ruby Units 0.3.1 # # Copyright 2006 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ # @@ -38,53 +38,128 @@ # end # Unit.setup class Unit < Numeric require 'units' # pre-generate hashes from unit definitions for performance. + VERSION = '0.3.0' @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@OUTPUT_MAP = {} - @@UNIT_VECTORS = {} + @@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>'] + UNITY = '<1>' + UNITY_ARRAY= [UNITY] + FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/ + 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 = ['<kelvin>'] + FARENHEIT = ['<farenheit>'] + RANKINE = ['<rankine>'] + CELCIUS = ['<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 - (UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value| + @@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[Regexp.escape(key)]=value[1] - value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key} + @@PREFIX_VALUES[key]=value[1] + for name in value[0] do + @@PREFIX_MAP[name]=key + end else - @@UNIT_VALUES[Regexp.escape(key)]={} - @@UNIT_VALUES[Regexp.escape(key)][:scalar]=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)] + @@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[Regexp.escape(key)]=value[0][0] + @@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/ + end - self.setup include Comparable - attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar + 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| - to_yaml_properties.each do |m| + for m in to_yaml_properties do map.add( m[1..-1], instance_variable_get( m ) ) end end end end @@ -99,120 +174,177 @@ # "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) - case options - when String: parse(options) + def initialize(*options) + 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 String: parse(options[0]) when Hash: - @scalar = options[:scalar] || 1 - @numerator = options[:numerator] || ["<1>"] - @denominator = options[:denominator] || [] - when Array: - parse("#{options[0]} #{options[1]}/#{options[2]}") + @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 - @numerator = @denominator = ['<1>'] + @scalar = options[0] + @numerator = @denominator = UNITY_ARRAY when Time: - @scalar = options.to_f + @scalar = options[0].to_f @numerator = ['<second>'] - @denominator = ['<1>'] + @denominator = UNITY_ARRAY else raise ArgumentError, "Invalid Unit Format" end self.update_base_scalar self.replace_temperature - self.freeze + + 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)/) + @@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.freeze + @numerator.freeze + @denominator.freeze + @base_scalar.freeze + @signature.freeze + @is_base.freeze + self end + + def kind + return @@KINDS[self.signature] + end + def self.cached + return @@cached_units + 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 true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/ + 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 - n.compact.each do |x| - return false unless x == '<1>' || - (@@UNIT_VALUES[Regexp.escape(x)] && - @@UNIT_VALUES[Regexp.escape(x)][:denominator].nil? && - @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x))) + for x in n.compact do + return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x))) end - return true + return @is_base = true end #convert to base SI units def to_base return self if self.is_base? -# return self.to('degK') if self.units =~ /temp(C|K|F|R)/ + cached = @@base_unit_cache[self.units] * self.scalar rescue nil + return cached if cached + num = [] den = [] - q = @scalar - @numerator.compact.each do |unit| - if @@PREFIX_VALUES[Regexp.escape(unit)] - q *= @@PREFIX_VALUES[Regexp.escape(unit)] + q = 1 + for unit in @numerator.compact do + if @@PREFIX_VALUES[unit] + q *= @@PREFIX_VALUES[unit] else - q *= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] 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] + 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 - @denominator.compact.each do |unit| - if @@PREFIX_VALUES[Regexp.escape(unit)] - q /= @@PREFIX_VALUES[Regexp.escape(unit)] + for unit in @denominator.compact do + if @@PREFIX_VALUES[unit] + q /= @@PREFIX_VALUES[unit] else - q /= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] 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] + 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 = ['<1>'] if num.empty? - - Unit.new(Unit.eliminate_terms(q,num,den)) + 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 scalar 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.to("in").scalar - "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" - when :lbs: - ounces = self.to("oz").scalar - "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" - when String - begin #first try a standard format string - target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/ - return self.to($2).to_s($1) if $2 - "#{($1 || '%g') % @scalar || 0} #{self.units}".strip - rescue #if that is malformed, try a time string - return (Time.gm(0) + self).strftime(target_units) - end + out = @output[target_units] rescue nil + if out + return out else - "#{'%g' % @scalar} #{self.units}".strip + case target_units + when :ft: + inches = self.to("in").scalar + out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" + when :lbs: + ounces = self.to("oz").scalar + 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 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 == ['<1>'] && @denominator == ['<1>']) + (@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) @@ -264,13 +396,12 @@ # 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. def +(other) if Unit === other if self =~ other then - a = self.to_base - b = other.to_base - Unit.new(:scalar=>(a.scalar + b.scalar), :numerator=>a.numerator, :denominator=>b.denominator).to(self) + q = self.zero? ? 1 : (self.scalar / self.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 @@ -283,40 +414,50 @@ # 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 - a = self.to_base - b = other.to_base - Unit.new(:scalar=>(a.scalar - b.scalar), :numerator=>a.numerator, :denominator=>b.denominator).to(self) + q = self.zero? ? 1 : (self.scalar / self.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) - x - y + y-x end end # Multiply two units. def *(other) - if Unit === other - Unit.new(Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)) + 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) - if Unit === other + case other + when Unit raise ZeroDivisionError if other.zero? - Unit.new(Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)) + 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 @@ -373,20 +514,20 @@ 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.clone - den = @denominator.clone + num = @numerator.dup + den = @denominator.dup - @numerator.uniq.each do |item| + 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 - @denominator.uniq.each do |item| + 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) @@ -417,11 +558,10 @@ 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 - return self.to_base.to(other) unless self.is_base? start_unit = self.units target_unit = other.units rescue other q=case start_unit when 'degC': case target_unit @@ -450,11 +590,12 @@ when 'tempK' : @scalar*(5.0/9.0) when 'tempF' : @scalar - 459.67 when 'tempR' : @scalar end else - raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}" + 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 @@ -464,18 +605,18 @@ 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[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:scalar] }.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)][:scalar] }.compact + 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[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:scalar] }.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)][:scalar] }.compact + 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) + Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature) end end alias :>> :to alias :convert_to :to @@ -487,67 +628,70 @@ 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 == ["<1>"] && @denominator == ["<1>"] + 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 == ["<1>"] + if @numerator == UNITY_ARRAY 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(num[index+1])]}" + 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[Regexp.escape(token)]}" if token + output_n << "#{@@OUTPUT_MAP[token]}" if token end end end - if @denominator == ['<1>'] + if @denominator == UNITY_ARRAY output_d = ['1'] else den.each_with_index do |token,index| - if token && @@PREFIX_VALUES[Regexp.escape(token)] then - output_d << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(den[index+1])]}" + 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[Regexp.escape(token)]}" if token + 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]}" : ''))} - "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip + 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 -@ - Unit.new([-@scalar,@numerator,@denominator]) + Unit.new(-@scalar,@numerator,@denominator) end # returns abs of scalar, without the units def abs return @scalar.abs end def ceil - Unit.new([@scalar.ceil, @numerator, @denominator]) + Unit.new(@scalar.ceil, @numerator, @denominator) end def floor - Unit.new([@scalar.floor, @numerator, @denominator]) + Unit.new(@scalar.floor, @numerator, @denominator) end # changes internal scalar to an integer, but retains the units # if unitless, returns an int def to_int return @scalar.to_int if unitless? - Unit.new([@scalar.to_int, @numerator, @denominator]) + Unit.new(@scalar.to_int, @numerator, @denominator) end # Tries to make a Time object from current unit def to_time Time.at(self) @@ -555,39 +699,39 @@ alias :time :to_time alias :to_i :to_int alias :truncate :to_int def round - Unit.new([@scalar.round, @numerator, @denominator]) + Unit.new(@scalar.round, @numerator, @denominator) end # true if scalar is zero def zero? return @scalar.zero? end # '5 min'.unit.ago def ago - Time.now - self rescue DateTime.now - self + 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 + time_point.time - self rescue time_point.datetime - self else - time_point - self + 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: (DateTime.now - time_point).unit('d').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 @@ -595,11 +739,11 @@ # 'min'.until(time) def until(time_point = ::Time.now) case time_point when Time: (time_point - Time.now).unit('s').to(self) - when DateTime: (time_point - DateTime.now).unit('d').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" @@ -608,29 +752,30 @@ # '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 + time_point.time + self rescue time_point.datetime + self else - time_point + self + time_point + self rescue time_point.to_datetime + self end end alias :after :from alias :from_now :from def succ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i q = @scalar.to_i.succ - Unit.new([q, @numerator, @denominator]) + Unit.new(q, @numerator, @denominator) 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 @@ -651,41 +796,46 @@ # 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 - 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.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size - vector[index] -= result.denominator.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size + 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.signature == 400 && self.units =~ /temp(R|K|F|C)/ + return self unless self.kind == :temperature && self.units =~ /temp(R|K|F|C)/ un = $1 - target = self.units @numerator = case un - when 'R' : ['<rankine>'] - when 'C' : ['<celcius>'] - when 'F' : ['<farenheit>'] - when 'K' : ['<kelvin>'] + when 'R' : RANKINE + when 'C' : CELCIUS + when 'F' : FARENHEIT + when 'K' : KELVIN end + @unit_name = nil r= self.to("tempK") - @numerator = r.numerator - @denominator = r.denominator - @scalar = r.scalar + copy(r) end - - + private def initialize_copy(other) - @numerator = other.numerator.clone - @denominator = other.denominator.clone + @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 # @@ -693,21 +843,22 @@ # 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.clone - den = d.clone + num = n.dup + den = d.dup - num.delete_if {|v| v == '<1>'} - den.delete_if {|v| v == '<1>'} + 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 @@ -716,11 +867,11 @@ i += 2 else k = num[i] i += 1 end - combined[k] += 1 unless k.nil? || k == '<1>' + combined[k] += 1 unless k.nil? || k == UNITY end j = 0 loop do break if j > den.size @@ -729,23 +880,23 @@ j += 2 else k = den[j] j += 1 end - combined[k] -= 1 unless k.nil? || k == '<1>' + combined[k] -= 1 unless k.nil? || k == UNITY end num = [] den = [] - combined.each do |key,value| + 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 = ["<1>"] if num.empty? - den = ["<1>"] if den.empty? + 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. @@ -757,96 +908,105 @@ # "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(unit_string="0") - @numerator = ['<1>'] - @denominator = ['<1>'] + def parse(passed_unit_string="0") + + unit_string = passed_unit_string.dup + if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ + unit_string = "#{$1} USD" + end + if unit_string =~ /(#{SCI_NUMBER})\s*%/ + unit_string = "#{$1} percent" + end + + 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!(/[<>]/,"") # Special processing for unusual unit strings # feet -- 6'5" - feet, inches = unit_string.scan(/(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/)[0] + feet, inches = unit_string.scan(FEET_INCH_REGEX)[0] if (feet && inches) result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches") - @scalar = result.scalar - @numerator = result.numerator - @denominator = result.denominator - @base_scalar = result.base_scalar + copy(result) return self end # weight -- 8 lbs 12 oz - pounds, oz = unit_string.scan(/(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/)[0] + pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0] if (pounds && oz) result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz") - @scalar = result.scalar - @numerator = result.numerator - @denominator = result.denominator - @base_scalar = result.base_scalar + copy(result) return self end - @scalar, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts + @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts - top.scan(/([^ \*]+)(?:\^|\*\*)([\d-]+)/).each do |item| + 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 (#{us})") unless us.empty? - bottom.gsub!(/([^* ]+)(?:\^|\*\*)(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom - if @scalar.empty? - if top =~ /[\dEe+.-]+/ - @scalar = top.to_f # need this for 'number only' initialization - else - @scalar = 1 # need this for 'unit only' intialization - end - else - @scalar = @scalar.to_f - end - - @numerator = top.scan(/(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/).delete_if {|x| x.empty?}.compact if top - @denominator = bottom.scan(/(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/).delete_if {|x| x.empty?}.compact if bottom - @numerator = @numerator.map do |item| - item.map {|x| Regexp.escape(x) if x} @@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| - item.map {|x| Regexp.escape(x) if x} @@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 = ['<1>'] if @numerator.empty? - @denominator = ['<1>'] if @denominator.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) + def +unit case unit - when Unit: unit_date_add(unit.to('day').scalar) + 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) + def -unit case unit - when Unit: unit_date_sub(unit.to('day').scalar) + 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 @@ -862,21 +1022,22 @@ end end class Object - def Unit(other) + 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) * Unit.new(other) : Unit.new(self) + other ? Unit.new(self, other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit end @@ -891,11 +1052,11 @@ end # make a string into a unit class String def to_unit(other = nil) - other ? Unit.new(self) >> other : Unit.new(self) + other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit alias :unit_format :% @@ -959,10 +1120,20 @@ 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 @@ -992,10 +1163,14 @@ 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: unit_add(other.to('s').scalar) when DateTime: unit_add(other.to_time) else @@ -1083,6 +1258,8 @@ module_function :cosh module_function :unit_tan module_function :tan module_function :unit_tanh module_function :tanh -end \ No newline at end of file +end + +Unit.setup