lib/ruby_units/ruby-units.rb in ruby-units-1.1.5 vs lib/ruby_units/ruby-units.rb in ruby-units-1.2.0.a

- old
+ new

@@ -1,13 +1,13 @@ -require 'mathn' -require 'rational' require 'date' -require 'parsedate' - +if RUBY_VERSION < "1.9" + require 'parsedate' + require 'rational' +end # = Ruby Units # -# Copyright 2006 by Kevin C. Olbrich, Ph.D. +# Copyright 2006-2010 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ # # http://www.sciwerks.org # @@ -38,11 +38,11 @@ # } # end # Unit.setup class Unit < Numeric # pre-generate hashes from unit definitions for performance. - VERSION = '1.1.5' + VERSION = '1.2.0.a' @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@ -131,16 +131,15 @@ end end @@OUTPUT_MAP[key]=value[0][0] end @@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| [prefix.length, prefix]}.reverse.join('|') - @@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| [unit.length, unit]}.reverse.join('|') + @@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit_name| [unit_name.length, unit]}.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} @@ -162,15 +161,17 @@ @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 ) ) + if RUBY_VERSION < "1.9" + 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 end @@ -200,42 +201,44 @@ initialize("#{options[0]} #{(options[1].units rescue options[1])}") end return end if options.size == 3 + options[1] = options[1].join if options[1].kind_of?(Array) + options[2] = options[2].join if options[2].kind_of?(Array) 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: + 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: + when Array initialize(*options[0]) return - when Numeric: + when Numeric @scalar = options[0] @numerator = @denominator = UNITY_ARRAY - when Time: + when Time @scalar = options[0].to_f @numerator = ['<second>'] @denominator = UNITY_ARRAY - when DateTime: + when DateTime @scalar = options[0].ajd @numerator = ['<day>'] @denominator = UNITY_ARRAY - when "": + when "" raise ArgumentError, "No Unit Specified" - when String: + when String parse(options[0]) else raise ArgumentError, "Invalid Unit Format" end self.update_base_scalar @@ -282,11 +285,11 @@ 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 && self.numerator.size == 1 && self.denominator == UNITY_ARRAY && self.units =~ /(deg|temp)K/ + return @is_base=true if self.degree? && self.numerator.size == 1 && self.denominator == UNITY_ARRAY && self.units =~ /(deg|temp)K/ 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 @@ -294,44 +297,46 @@ # 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? - if self.units =~ /\A(deg|temp)(C|F|K|C)\Z/ + if self.units =~ /\A(deg|temp)(C|F|K|C)\Z/ @signature = 400 base = case self.units - when /temp/ : self.to('tempK') - when /deg/ : self.to('degK') + when /temp/ + self.to('tempK') + when /deg/ + self.to('degK') end return base end 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 + 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 + 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 @@ -354,19 +359,19 @@ out = @output[target_units] if out return out else case target_units - when :ft : + when :ft inches = self.to("in").scalar.to_int out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" - when :lbs : + when :lbs ounces = self.to("oz").scalar.to_int out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" when String out = case target_units - when /(%[-+\.\w\d#]+)\s*(.+)*/ #format string like '%0.2f in' + when /(%[\-+\.\w#]+)\s*(.+)*/ #format string like '%0.2f in' begin if $2 #unit specified, need to convert self.to($2).to_s($1) else "#{$1 % @scalar} #{$2 || self.units}".strip @@ -379,11 +384,11 @@ else raise "unhandled case" end else out = case @scalar - when Rational : + when Rational "#{@scalar} #{self.units}" else "#{'%g' % @scalar} #{self.units}" end.strip end @@ -398,13 +403,20 @@ self.to_s end # true if unit is a 'temperature', false if a 'degree' or anything else def is_temperature? - return true if self.signature == 400 && self.units =~ /temp/ + self.is_degree? && self.units =~ /temp/ end + alias :temperature? :is_temperature? + # true if a degree unit or equivalent. + def is_degree? + self.kind == :temperature + end + alias :degree? :is_degree? + # returns the 'degree' unit associated with a temperature unit # '100 tempC'.unit.temperature_scale #=> 'degC' def temperature_scale return nil unless self.is_temperature? self.units =~ /temp(C|F|R|K)/ @@ -418,18 +430,19 @@ 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 0: self.base_scalar <=> 0 - when Unit: + case + when other.zero? && !self.is_temperature? + return self.base_scalar <=> 0 + when Unit === other raise ArgumentError, "Incompatible Units" unless self =~ other - self.base_scalar <=> other.base_scalar + return self.base_scalar <=> other.base_scalar else x,y = coerce(other) - x <=> y + return 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 @@ -439,11 +452,12 @@ # if you want to do a regexp on the unit string do this ... # unit.units =~ /regexp/ def =~(other) return true if self == 0 || other == 0 case other - when Unit : self.signature == other.signature + when Unit + self.signature == other.signature else x,y = coerce(other) x =~ y end end @@ -455,11 +469,12 @@ # # 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) + when Unit + (self.scalar == other.scalar) && (self.units == other.units) else x,y = coerce(other) x === y end end @@ -471,48 +486,49 @@ # 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 case - when self.zero? : other.dup - when self =~ other : + when self.zero? + other.dup + when self =~ other raise ArgumentError, "Cannot add two temperatures" if ([self, other].all? {|x| x.is_temperature?}) if [self, other].any? {|x| x.is_temperature?} - case self.is_temperature? - when true: + if self.is_temperature? Unit.new(:scalar => (self.scalar + other.to(self.temperature_scale).scalar), :numerator => @numerator, :denominator=>@denominator, :signature => @signature) else Unit.new(:scalar => (other.scalar + self.to(other.temperature_scale).scalar), :numerator => other.numerator, :denominator=>other.denominator, :signature => other.signature) end else @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.to_base.scalar)) Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) end else - raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" + raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end elsif Time === other other + self else - x,y = coerce(other) - y + x + 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 - case - when self.zero? : -other.dup - when self =~ other : + case + when self.zero? + -other.dup + when self =~ other case - when [self, other].all? {|x| x.is_temperature?} : + when [self, other].all? {|x| x.is_temperature?} Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).to(self.temperature_scale) - when self.is_temperature? : + when self.is_temperature? Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<temp-K>'], :denominator => UNITY_ARRAY, :signature => @signature).to(self) - when other.is_temperature? : + when other.is_temperature? raise ArgumentError, "Cannot subtract a temperature from a differential degree unit" else @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.scalar/self.units.unit.to_base.scalar)) Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature) end @@ -559,10 +575,22 @@ else x,y = coerce(other) y / x end end + + # divide two units and return quotient and remainder + # when both units are in the same units we just use divmod on the raw scalars + # otherwise we use the scalar of the base unit which will be a float + def divmod(other) + raise ArgumentError, "Incompatible Units" unless self =~ other + if self.units == other.units + return self.scalar.divmod(other.scalar) + else + return self.to_base.scalar.divmod(other.to_base.scalar) + 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 @@ -576,15 +604,15 @@ return Unit("1") if other.zero? return self if other == 1 return self.inverse if other == -1 end case other - when Rational: + when Rational self.power(other.numerator).root(other.denominator) - when Integer: + when Integer self.power(other) - when Float: + 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 @@ -622,17 +650,17 @@ 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))} + r.times {|y| 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))} + r.times {|y| den.delete_at(den.index(item))} end q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n) Unit.new(:scalar=>q,:numerator=>num,:denominator=>den) end @@ -650,47 +678,56 @@ # 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, fahrenheit, and Rankine scales. + # Supports Kelvin, Celsius, fahrenheit, 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.is_temperature?) || (String === other && other =~ /temp(K|C|R|F)/) - raise ArgumentError, "Receiver is not a temperature unit" unless self.signature == 400 + raise ArgumentError, "Receiver is not a temperature unit" unless self.degree? start_unit = self.units target_unit = other.units rescue other unless @base_scalar @base_scalar = case start_unit - when 'tempC' : @scalar + 273.15 - when 'tempK' : @scalar - when 'tempF' : (@scalar+459.67)*(5.0/9.0) - when 'tempR' : @scalar*(5.0/9.0) + when 'tempC' + @scalar + 273.15 + when 'tempK' + @scalar + when 'tempF' + (@scalar+459.67)*(5.0/9.0) + when 'tempR' + @scalar*(5.0/9.0) end end q= case target_unit - when 'tempC' : @base_scalar - 273.15 - when 'tempK' : @base_scalar - when 'tempF' : @base_scalar * (9.0/5.0) - 459.67 - when 'tempR' : @base_scalar * (9.0/5.0) - end - + when 'tempC' + @base_scalar - 273.15 + when 'tempK' + @base_scalar + when 'tempF' + @base_scalar * (9.0/5.0) - 459.67 + when 'tempR' + @base_scalar * (9.0/5.0) + end + Unit.new("#{q} #{target_unit}") 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 + 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 @@ -704,11 +741,11 @@ 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" + raise RuntimeError, "Can't convert to Float unless unitless (#{self.to_s}). Use Unit#scalar" end # converts the unit back to a complex if it is unitless. Otherwise raises an exception def to_c @@ -738,17 +775,17 @@ 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 + 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 @@ -809,11 +846,11 @@ Unit.new(@scalar.round, @numerator, @denominator) end # true if scalar is zero def zero? - return @scalar.zero? + return self.to_base.scalar.zero? end # '5 min'.unit.ago def ago self.before @@ -831,27 +868,30 @@ 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) + 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.to_datetime(: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) + when Time + (time_point - Time.now).unit('s').to(self) + when DateTime, Date + (time_point - DateTime.now).unit('d').to(self) + when String + (time_point.to_datetime(:context=>:future) - DateTime.now).unit('d').to(self) else raise ArgumentError, "Must specify a Time, DateTime, or String" end end @@ -880,11 +920,12 @@ def coerce(other) if other.respond_to? :to_unit return [other.to_unit, self] end case other - when Unit : [other, self] + when Unit + [other, self] else [Unit.new(other), self] end end @@ -904,26 +945,25 @@ 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 + return self.to_base.unit_signature_vector unless self.is_base? + 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 - 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 + for element in @denominator + if r=@@ALL_UNIT_DEFINITIONS[element] + n = SIGNATURE_VECTOR.index(r[2]) + vector[n] = vector[n] - 1 if n end - vector + end + vector end private def initialize_copy(other) @@ -982,12 +1022,14 @@ num = [] den = [] for key, value in combined do case - when value > 0 : value.times {num << key} - when value < 0 : value.abs.times {den << key} + 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} @@ -1081,11 +1123,13 @@ @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}/,"") + 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