lib/ruby_units/ruby-units.rb in ruby-units-1.0.2 vs lib/ruby_units/ruby-units.rb in ruby-units-1.1.0

- old
+ new

@@ -1,22 +1,10 @@ require 'mathn' require 'rational' require 'date' require 'parsedate' - -=begin -require 'math' - -require 'object_class' -require 'array_class' -require 'string_class' -require 'date_class' -require 'time_class' -require 'numeric_class' -=end - # = Ruby Units # # Copyright 2006 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ @@ -50,18 +38,18 @@ # } # end # Unit.setup class Unit < Numeric # pre-generate hashes from unit definitions for performance. - VERSION = '1.0.2' + VERSION = '1.1.0' @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@OUTPUT_MAP = {} - @@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>'] + @@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<temp-K>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>'] 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)/ @@ -74,13 +62,13 @@ BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/ UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/ COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/ RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/ KELVIN = ['<kelvin>'] - FARENHEIT = ['<farenheit>'] + FAHRENHEIT = ['<fahrenheit>'] RANKINE = ['<rankine>'] - CELCIUS = ['<celcius>'] + CELSIUS = ['<celsius>'] SIGNATURE_VECTOR = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle, :capacitance] @@KINDS = { -312058=>:resistance, -312038=>:inductance, @@ -222,11 +210,10 @@ 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 @@ -249,11 +236,12 @@ when String: parse(options[0]) else raise ArgumentError, "Invalid Unit Format" end self.update_base_scalar - self.replace_temperature + raise ArgumentError, "Temperature out of range" if self.is_temperature? && self.base_scalar < 0 + 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})|i\s?(.+)?|&plusmn;|\+\/-/) @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty? @@ -288,11 +276,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 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/ + return @is_base=true if @signature == 400 && 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 @@ -300,13 +288,22 @@ # 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 + 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') + 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] @@ -383,10 +380,20 @@ def inspect(option=nil) return super() if option == :dump self.to_s end + def is_temperature? + return true if self.signature == 400 && self.units =~ /temp/ + end + + def temperature_scale + return nil unless self.is_temperature? + self.units =~ /temp(C|F|R|K)/ + "deg#{$1}" + 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 @@ -447,12 +454,22 @@ def +(other) if Unit === other case when self.zero? : other.dup when self =~ other : - @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) + raise ArgumentError, "Cannot add two temperatures" if (self.is_temperature? && other.is_temperature?) + if [self, other].any? {|x| x.is_temperature?} + case self.is_temperature? + when true: + 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}')" end elsif Time === other other + self @@ -467,12 +484,21 @@ def -(other) if Unit === other case when self.zero? : -other.dup when self =~ other : - @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) + case + 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? : + Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<temp-K>'], :denominator => UNITY_ARRAY, :signature => @signature).to(self) + when other.is_temperature? : + raise ArgumentError, "Cannot subtract a temperature from a differential 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 else raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end elsif Time === other other - self @@ -484,10 +510,11 @@ # Multiply two units. def *(other) case other when Unit + raise ArgumentError, "Cannot multiply by temperatures" if [other,self].any? {|x| x.is_temperature?} 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) @@ -501,10 +528,11 @@ # Throws an exception if divisor is 0 def /(other) case other when Unit raise ZeroDivisionError if other.zero? + raise ArgumentError, "Cannot divide with temperatures" if [other,self].any? {|x| x.is_temperature?} 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? @@ -522,10 +550,11 @@ # 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) + raise ArgumentError, "Cannot exponentiate a temperature" if self.is_temperature? if Numeric === other return Unit("1") if other.zero? return self if other == 1 return self.inverse if other == -1 end @@ -544,10 +573,11 @@ end end # returns the unit raised to the n-th power. Integers only def power(n) + raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature? 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 @@ -558,10 +588,11 @@ 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, "Cannot take the root of a temperature" if self.is_temperature? 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 @@ -600,57 +631,75 @@ # 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. + # Supports Kelvin, Celcius, 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.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 + 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 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) + 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 + +=begin q=case start_unit - when 'degC': + when /\A(temp|deg)C\Z/: 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': + when /\A(temp|deg)K\Z/: 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': + when /\A(temp|deg)F\Z/: 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': + when /\A(temp|deg)R\Z/: 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}") +=end + #target_unit =~ /temp(C|K|F|R)/ + Unit.new("#{q} #{target_unit}") else case other when Unit: return self if other.units == self.units target = other @@ -724,13 +773,13 @@ return out end # negates the scalar of the Unit def -@ + #raise ArgumentError, "Cannot negate an absolute temperature" if self.is_temperature? && ['degK','degR'].include?(self.temperature_scale) return -@scalar if self.unitless? - #Unit.new(-@scalar,@numerator,@denominator) - -1 * self.dup + self.dup * -1 end # returns abs of scalar, without the units def abs return @scalar.abs @@ -765,13 +814,17 @@ 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) + DateTime.new0(self.to('d').scalar) end + def to_date + Date.new0(self.to('d').scalar) + end + def round return @scalar.round if self.unitless? Unit.new(@scalar.round, @numerator, @denominator) end @@ -889,23 +942,9 @@ 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)