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