lib/ruby-units.rb in ruby-units-0.1.1 vs lib/ruby-units.rb in ruby-units-0.2.0

- old
+ new

@@ -1,6 +1,9 @@ -# = Ruby Units 0.1.1 +require 'mathn' +require 'rational' + +# = Ruby Units 0.2.0 # # Copyright 2006 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ # @@ -49,11 +52,11 @@ if value[2] == :prefix then @@PREFIX_VALUES[Regexp.escape(key)]=value[1] value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key} else @@UNIT_VALUES[Regexp.escape(key)]={} - @@UNIT_VALUES[Regexp.escape(key)][:quantity]=value[1] + @@UNIT_VALUES[Regexp.escape(key)][: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)] @@ -65,75 +68,99 @@ end self.setup include Comparable - attr_reader :quantity, :numerator, :denominator, :signature, :base_quantity + attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar + + def to_yaml_properties + %w{@scalar @numerator @denominator @signature @base_scalar} + 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| + 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 quantity 1 with units 'GPa' - # 6'4" -- recognized as 6 feet + 4 inches + # "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) + def initialize(options) case options when String: parse(options) when Hash: - @quantity = options[:quantity] || 1 + @scalar = options[:scalar] || 1 @numerator = options[:numerator] || ["<1>"] @denominator = options[:denominator] || [] - when Array: parse("#{options[0]} #{options[1]}/#{options[2]}") - when Numeric: parse(options.to_s) + when Array: + parse("#{options[0]} #{options[1]}/#{options[2]}") + when Numeric: + @scalar = options + @numerator = @denominator = ['<1>'] + when Time: + @scalar = options.to_f + @numerator = ['<second>'] + @denominator = ['<1>'] else raise ArgumentError, "Invalid Unit Format" end - self.update_base_quantity - self.unit_signature + self.update_base_scalar self.freeze end def initialize_copy(other) @numerator = other.numerator.clone @denominator = other.denominator.clone end # Returns 'true' if the Unit is represented in base units def is_base? + return true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/ n = @numerator + @denominator - n.each do |x| - return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x))) + 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))) end return true end #convert to base SI units def to_base return self if self.is_base? num = [] den = [] - q = @quantity.to_f - @numerator.each do |unit| + q = @scalar + @numerator.compact.each do |unit| if @@PREFIX_VALUES[Regexp.escape(unit)] q *= @@PREFIX_VALUES[Regexp.escape(unit)] else - q *= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)] + 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] end end - @denominator.each do |unit| + @denominator.compact.each do |unit| if @@PREFIX_VALUES[Regexp.escape(unit)] q /= @@PREFIX_VALUES[Regexp.escape(unit)] else - q /= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)] + 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] end end @@ -143,138 +170,217 @@ Unit.new(Unit.eliminate_terms(q,num,den)) end # Generate human readable output. - # If the name of a unit is passed, the quantity will first be converted to the target unit before output. + # 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 >> "in").to_f + inches = self.to("in").scalar "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" when :lbs: - ounces = (self >> "oz").to_f + ounces = self.to("oz").scalar "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" else target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/ - format_string = "#{$1}" if $1 - units = $2 - return (self >> units).to_s(format_string) if units - "#{(format_string || '%g') % @quantity} #{self.to_unit}".strip + return self.to($2).to_s($1) if $2 + "#{($1 || '%g') % @scalar || 0} #{self.units}".strip end end + def inspect(option=nil) + return super() if option == :dump + self.to_s + end + + # returns true if no associated units + def unitless? + (@numerator == ['<1>'] && @denominator == ['<1>']) + end + # Compare two Unit objects. Throws an exception if they are not of compatible types. # Comparisons are done based on the value of the unit in base SI units. def <=>(other) - raise ArgumentError, "Incompatible Units" unless self =~ other - return self.base_quantity <=> other.base_quantity + 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 quantity part + # 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.to_unit =~ /regexp/ + # 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.quantity == other.quantity) && (self.to_unit == other.to_unit) + when Unit: (self.scalar == other.scalar) && (self.units == other.units) else x,y = coerce(other) x === y end end - # Add two units together. Result is same units as receiver and quantity and base_quantity are updated appropriately + 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. def +(other) if Unit === other if self =~ other then - q = @quantity + (other >> self).quantity - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + q = @scalar + other.to(self).scalar + Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator) else raise ArgumentError, "Incompatible Units" end else x,y = coerce(other) x + y end end - # Subtract two units. Result is same units as receiver and quantity and base_quantity are updated appropriately + # 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 = @quantity - (other >> self).quantity - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + q = @scalar - other.to(self).scalar + Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator) else raise ArgumentError, "Incompatible Units" end else x,y = coerce(other) x - y end end # Multiply two units. - # Throws an exception if multiplier is not a Unit or Numeric def *(other) if Unit === other - Unit.new(Unit.eliminate_terms(@quantity*other.quantity, @numerator + other.numerator ,@denominator + other.denominator)) + Unit.new(Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)) else x,y = coerce(other) x * y end end # Divide two units. - # Throws an exception if divisor is not a Unit or Numeric + # Throws an exception if divisor is 0 def /(other) if Unit === other - Unit.new(Unit.eliminate_terms(@quantity/other.quantity, @numerator + other.denominator ,@denominator + other.numerator)) + raise ZeroDivisionError if other.zero? + Unit.new(Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)) else x,y = coerce(other) y / x end end # Exponentiate. Only takes integer powers. - # Note that anything raised to the power of 0 results in a Unit object with a quantity of 1, and no units. + # 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) - raise ArgumentError, "Exponent must be Integer" unless Integer === other - case - when other.to_i > 0 : (1..other.to_i).inject(Unit.new("1")) {|product, n| product * self} - when other.to_i == 0 : Unit.new("1") - when other.to_i < 0 : (1..-other.to_i).inject(Unit.new("1")) {|product, n| product / self} + 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.clone + den = @denominator.clone + + @numerator.uniq.each do |item| + 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| + 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([q,num,den]) + end + # returns inverse of Unit (1/unit) def inverse - (Unit.new("1") / self) + Unit("1") / self end # convert to a specified unit string or to the same units as another Unit # # unit >> "kg" will covert to kilograms @@ -288,80 +394,82 @@ # from one temperature unit to another, the proper temperature offsets will be used. # Supports Kelvin, Celcius, Farenheit, and Rankine scales. # # Note that if temperature is part of a compound unit, the temperature will be treated as a differential # and the units will be scaled appropriately. - def >>(other) - case other.class.to_s - when 'Unit': target = other - when 'String': target = Unit.new(other) - else - raise ArgumentError, "Unknown target units" - end - raise ArgumentError, "Incompatible Units" unless self =~ target - if target.signature == 400 then # special handling for temperature conversions - q=case self.numerator[0] - when '<celcius>': - case target.numerator[0] - when '<celcius>' : @quantity - when '<kelvin>' : @quantity + 273.15 - when '<farenheit>': @quantity * (9.0/5.0) + 32.0 - when '<rankine>' : @quantity * (9.0/5.0) + 491.67 + def to(other) + return self if other.nil? + return self if TrueClass === other + return self if FalseClass === other + if 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? + return self.to_base.to("tempF") if @numerator.size > 1 || @denominator != ['<1>'] + q=case + when @numerator.include?('<celcius>'): + case other + 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 @numerator.include?( '<kelvin>'): + case other + 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 @numerator.include?( '<farenheit>'): + case other + 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 @numerator.include?( '<rankine>'): + case other + 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 - raise ArgumentError, "Unknown temperature conversion requested" - end - when '<kelvin>': - case target.numerator[0] - when '<celcius>' : @quantity - 273.15 - when '<kelvin>' : @quantity - when '<farenheit>': @quantity * (9.0/5.0) - 459.67 - when '<rankine>' : @quantity * (9.0/5.0) - else - raise ArgumentError, "Unknown temperature conversion requested" - end - when '<farenheit>': - case target.numerator[0] - when '<celcius>' : (@quantity-32)*(5.0/9.0) - when '<kelvin>' : (@quantity+459.67)*(5.0/9.0) - when '<farenheit>': @quantity - when '<rankine>' : @quantity + 459.67 - else - raise ArgumentError, "Unknown temperature conversion requested" - end - when '<rankine>': - case target.numerator[0] - when '<celcius>' : @quantity*(5.0/9.0) -273.15 - when '<kelvin>' : @quantity*(5.0/9.0) - when '<farenheit>': @quantity - 459.67 - when '<rankine>' : @quantity - else - raise ArgumentError, "Unknown temperature conversion requested" - end - else - raise ArgumentError, "Unknown temperature conversion requested" + raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}" end - Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator) + Unit.new("#{q} deg#{$1}") else - one = @numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact - two = @denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact + 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[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 v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} - one = target.numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact - two = target.denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact + 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 y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n} - q = @quantity * v/y - Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator) + q = @scalar * v/y + Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator) end end + alias :>> :to # calculates the unit signature vector used by unit_signature def unit_signature_vector - result = self.to_base + 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.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size - vector[index] -= result.denominator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size + 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 end vector end # calculates the unit signature id for use in comparing compatible units and simplification @@ -377,139 +485,163 @@ vector.each_with_index {|item,index| vector[index] = item * 20**index} @signature=vector.inject(0) {|sum,n| sum+n} end # Eliminates terms in the passed numerator and denominator. Expands out prefixes and applies them to the - # quantity. Returns a hash that can be used to initialize a new Unit object. + # scalar. Returns a hash that can be used to initialize a new Unit object. def self.eliminate_terms(q, n, d) num = n.clone den = d.clone - # cancel terms in both numerator and denominator - num.each_with_index do |item,index| - if i=den.index(item) - num.delete_at(index) - den.delete_at(i) + num.delete_if {|v| v == '<1>'} + den.delete_if {|v| v == '<1>'} + 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 == '<1>' end - num = num.flatten.compact - den = den.flatten.compact - - # substitute in SI prefix multipliers and numerical constants - num.each_with_index do |item, index| - if item =~ /<([\dEe+-.]+)>/ - q *= $1.to_f - num.delete_at(index) - elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)] - q *= multiplier - num.delete_at(index) - end + 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 == '<1>' end - - den.each_with_index do |item, index| - if item =~ /<([\dEe+-.]+)>/ - q /= $1.to_f - den.delete_at(index) - elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)] - q /= multiplier - den.delete_at(index) + + num = [] + den = [] + combined.each do |key,value| + 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? - {:quantity=>q, :numerator=>num, :denominator=>den} + den = ["<1>"] if den.empty? + {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact} end - # returns the quantity part of the Unit + # returns the scalar part of the Unit def to_f - @quantity + 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 quantity - def to_unit + # returns the 'unit' part of the Unit object without the scalar + def units return "" if @numerator == ["<1>"] && @denominator == ["<1>"] - output_n = [] - num = @numerator.clone - den = @denominator.clone + output_n = [] + output_d =[] + num = @numerator.clone.compact + den = @denominator.clone.compact if @numerator == ["<1>"] output_n << "1" else num.each_with_index do |token,index| if token && @@PREFIX_VALUES[Regexp.escape(token)] then - output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(@numerator[index+1])]}" + output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(num[index+1])]}" num[index+1]=nil else output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token end end end - output_d = den.map do |token| - @@PREFIX_MAP[Regexp.escape(token)] ? @@OUTPUT_MAP[Regexp.escape(token)] : "#{@@OUTPUT_MAP[Regexp.escape(token)]} " + if @denominator == ['<1>'] + 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])]}" + den[index+1]=nil + else + output_d << "#{@@OUTPUT_MAP[Regexp.escape(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 end - # negates the quantity of the Unit + # negates the scalar of the Unit def -@ - q = -self.quantity - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + Unit.new([-@scalar,@numerator,@denominator]) end - # returns abs of quantity, without the units + # returns abs of scalar, without the units def abs - return @quantity.abs + return @scalar.abs end def ceil - q = self.quantity.ceil - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + Unit.new([@scalar.ceil, @numerator, @denominator]) end def floor - q = self.quantity.floor - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.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 - q = self.quantity.to_int - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + return @scalar.to_int if unitless? + Unit.new([@scalar.to_int, @numerator, @denominator]) end + def to_time + Time.at(self) + end + alias :time :to_time alias :to_i :to_int alias :truncate :to_int def round - q = self.quantity.round - Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator) + Unit.new([@scalar.round, @numerator, @denominator]) end - # true if quantity is zero + # true if scalar is zero def zero? - return @quantity.zero? + return @scalar.zero? end - - # returns self if zero? is false, nil otherwise - #def nonzero? - # self.zero? ? nil : self - #end - - def update_base_quantity - @base_quantity = self.is_base? ? @quantity : self.to_base.quantity - self + + def succ + raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i + q = @scalar.to_i.succ + Unit.new([q, @numerator, @denominator]) end + + def update_base_scalar + if self.is_base? + @base_scalar = @scalar + @signature = unit_signature + else + base = self.to_base + @base_scalar = base.scalar + @signature = base.signature + end + end def coerce(other) case other when Unit : [other, self] - when String : [Unit.new(other), self] - when Array: [Unit.new(other.join('*')), self] - when Numeric : [Unit.new(other.to_s), self] else - raise ArgumentError, "Invalid Unit Definition" + [Unit.new(other), self] end end private @@ -519,63 +651,62 @@ # "5.6 kg*m*s^-2" # "5.6 kilogram*meter*second^-2" # "2.2 kPa" # "37 degC" # "1" -- creates a unitless constant with value 1 - # "GPa" -- creates a unit with quantity 1 with units 'GPa' + # "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>'] + 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(/(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/)[0] if (feet && inches) result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches") - @quantity = result.quantity + @scalar = result.scalar @numerator = result.numerator @denominator = result.denominator - @base_quantity = result.base_quantity + @base_scalar = result.base_scalar return self end # weight -- 8 lbs 12 oz - pounds, oz = unit_string.scan(/(\d+)[\s|#|lbs|pounds|,]+(\d+)[\s*|oz|ounces]/)[0] + pounds, oz = unit_string.scan(/(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/)[0] if (pounds && oz) result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz") - @quantity = result.quantity + @scalar = result.scalar @numerator = result.numerator @denominator = result.denominator - @base_quantity = result.base_quantity + @base_scalar = result.base_scalar return self end - - @quantity, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts - - top.scan(/([^ \*]+)\^([\d-]+)/).each do |item| + @scalar, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts + + top.scan(/([^ \*]+)(?:\^|\*\*)([\d-]+)/).each do |item| n = item[1].to_i x = "#{item[0]} " case - when n>=0 : top.gsub!(/([^ \*]+)\^(\d+)/) {|s| x * n} - when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!("#{item[0]}^#{item[1]}","") + 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!(/([^* ]+)\^(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom - - if @quantity.empty? + + bottom.gsub!(/([^* ]+)(?:\^|\*\*)(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom + if @scalar.empty? if top =~ /[\dEe+.-]+/ - @quantity = top.to_f # need this for 'number only' initialization + @scalar = top.to_f # need this for 'number only' initialization else - @quantity = 1 # need this for 'unit only' intialization + @scalar = 1 # need this for 'unit only' intialization end else - @quantity = @quantity.to_f + @scalar = @scalar.to_f end - + @numerator = top.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if top @denominator = bottom.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if bottom @numerator = @numerator.map do |item| item.map {|x| Regexp.escape(x) if x} @@ -588,35 +719,91 @@ end.flatten.compact.delete_if {|x| x.empty?} @numerator = ['<1>'] if @numerator.empty? @denominator = ['<1>'] if @denominator.empty? self + end +end + +if defined? Uncertain + class Uncertain + def to_unit(other=nil) + other ? Unit.new(self).to(other) : Unit.new(self) + end end - end + class Object def Unit(other) other.to_unit end end class Numeric def to_unit(other = nil) - other ? Unit.new(self.to_s) >> other : Unit.new(self.to_s) + other ? Unit.new(self) * Unit.new(other) : Unit.new(self) end alias :unit :to_unit end + class Array def to_unit(other = nil) - other ? Unit.new("#{self[0]} #{self[1]}/#{self[2]}") >> other : Unit.new("#{self[0]} #{self[1]}/#{self[2]}") + other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit end class String def to_unit(other = nil) other ? Unit.new(self) >> other : Unit.new(self) end alias :unit :to_unit + alias :unit_format :% + + def %(*args) + if Unit === args[0] + args[0].to_s(self) + else + unit_format(*args) + end + end +end + +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 = "s") + other ? Unit.new(self.to_f) * Unit.new(other) : Unit.new(self.to_f) + end + alias :unit :to_unit + + alias :unit_add :+ + def +(other) + if Unit === other + self.unit + other + else + unit_add(other) + end + end + + alias :unit_sub :- + def -(other) + if Unit === other + self.unit - other + else + unit_sub(other) + end + end end \ No newline at end of file