lib/ruby-units.rb in ruby-units-0.2.3 vs lib/ruby-units.rb in ruby-units-0.3.1
- old
+ new
@@ -1,10 +1,10 @@
require 'mathn'
require 'rational'
require 'date'
require 'parsedate'
-# = Ruby Units 0.2.3
+# = Ruby Units 0.3.1
#
# Copyright 2006 by Kevin C. Olbrich, Ph.D.
#
# See http://rubyforge.org/ruby-units/
#
@@ -38,53 +38,128 @@
# end
# Unit.setup
class Unit < Numeric
require 'units'
# pre-generate hashes from unit definitions for performance.
+ VERSION = '0.3.0'
@@USER_DEFINITIONS = {}
@@PREFIX_VALUES = {}
@@PREFIX_MAP = {}
@@UNIT_MAP = {}
@@UNIT_VALUES = {}
@@OUTPUT_MAP = {}
- @@UNIT_VECTORS = {}
+ @@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>']
+ UNITY = '<1>'
+ UNITY_ARRAY= [UNITY]
+ FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/
+ LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/
+ SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)}
+ NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/
+ UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/
+ TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/
+ BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/
+
+ KELVIN = ['<kelvin>']
+ FARENHEIT = ['<farenheit>']
+ RANKINE = ['<rankine>']
+ CELCIUS = ['<celcius>']
+ SIGNATURE_VECTOR = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle, :capacitance]
+ @@KINDS = {
+ -312058=>:resistance,
+ -312038=>:inductance,
+ -152040=>:magnetism,
+ -152038=>:magnetism,
+ -152058=>:potential,
+ -39=>:acceleration,
+ -38=>:radiation,
+ -20=>:frequency,
+ -19=>:speed,
+ -18=>:viscosity,
+ 0=>:unitless,
+ 1=>:length,
+ 2=>:area,
+ 3=>:volume,
+ 20=>:time,
+ 400=>:temperature,
+ 7942=>:power,
+ 7959=>:pressure,
+ 7962=>:energy,
+ 7979=>:viscosity,
+ 7981=>:force,
+ 7997=>:mass_concentration,
+ 8000=>:mass,
+ 159999=>:magnetism,
+ 160000=>:current,
+ 160020=>:charge,
+ 312058=>:resistance,
+ 3199980=>:activity,
+ 3199997=>:molar_concentration,
+ 3200000=>:substance,
+ 63999998=>:illuminance,
+ 64000000=>:luminous_power,
+ 1280000000=>:currency,
+ 25600000000=>:memory,
+ 511999999980=>:angular_velocity,
+ 512000000000=>:angle,
+ 10240000000000=>:capacitance,
+ }
+
+ @@cached_units = {}
+ @@base_unit_cache = {}
+
def self.setup
- (UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value|
+ @@ALL_UNIT_DEFINITIONS = UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)
+ for unit in (@@ALL_UNIT_DEFINITIONS) do
+ key, value = unit
if value[2] == :prefix then
- @@PREFIX_VALUES[Regexp.escape(key)]=value[1]
- value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key}
+ @@PREFIX_VALUES[key]=value[1]
+ for name in value[0] do
+ @@PREFIX_MAP[name]=key
+ end
else
- @@UNIT_VALUES[Regexp.escape(key)]={}
- @@UNIT_VALUES[Regexp.escape(key)][:scalar]=value[1]
- @@UNIT_VALUES[Regexp.escape(key)][:numerator]=value[3] if value[3]
- @@UNIT_VALUES[Regexp.escape(key)][:denominator]=value[4] if value[4]
- value[0].each {|x| @@UNIT_MAP[Regexp.escape(x)]=key}
- @@UNIT_VECTORS[value[2]] = [] unless @@UNIT_VECTORS[value[2]]
- @@UNIT_VECTORS[value[2]] = @@UNIT_VECTORS[value[2]]+[Regexp.escape(key)]
+ @@UNIT_VALUES[key]={}
+ @@UNIT_VALUES[key][:scalar]=value[1]
+ @@UNIT_VALUES[key][:numerator]=value[3] if value[3]
+ @@UNIT_VALUES[key][:denominator]=value[4] if value[4]
+ for name in value[0] do
+ @@UNIT_MAP[name]=key
+ end
end
- @@OUTPUT_MAP[Regexp.escape(key)]=value[0][0]
+ @@OUTPUT_MAP[key]=value[0][0]
end
@@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| prefix.length}.reverse.join('|')
@@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| unit.length}.reverse.join('|')
+ @@UNIT_MATCH_REGEX = /(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/
+
end
- self.setup
include Comparable
- attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar
+ attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar, :base_numerator, :base_denominator, :output, :unit_name
def to_yaml_properties
%w{@scalar @numerator @denominator @signature @base_scalar}
end
+
+ def copy(from)
+ @scalar = from.scalar
+ @numerator = from.numerator
+ @denominator = from.denominator
+ @is_base = from.is_base?
+ @signature = from.signature
+ @base_scalar = from.base_scalar
+ @output = from.output rescue nil
+ @unit_name = from.unit_name rescue nil
+ end
# basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
# before YAML'izing it.
def to_yaml( opts = {} )
YAML::quick_emit( object_id, opts ) do |out|
out.map( taguri, to_yaml_style ) do |map|
- to_yaml_properties.each do |m|
+ for m in to_yaml_properties do
map.add( m[1..-1], instance_variable_get( m ) )
end
end
end
end
@@ -99,120 +174,177 @@
# "1" -- creates a unitless constant with value 1
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
# 6'4" -- recognized as 6 feet + 4 inches
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
#
- def initialize(options)
- case options
- when String: parse(options)
+ def initialize(*options)
+ if options.size == 2
+ begin
+ cached = @@cached_units[options[1]] * options[0]
+ copy(cached)
+ rescue
+ initialize("#{options[0]} #{options[1]}")
+ end
+ return
+ end
+ if options.size == 3
+ begin
+ cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
+ copy(cached)
+ rescue
+ initialize("#{options[0]} #{options[1]}/#{options[2]}")
+ end
+ return
+ end
+
+
+ case options[0]
+ when String: parse(options[0])
when Hash:
- @scalar = options[:scalar] || 1
- @numerator = options[:numerator] || ["<1>"]
- @denominator = options[:denominator] || []
- when Array:
- parse("#{options[0]} #{options[1]}/#{options[2]}")
+ @scalar = options[0][:scalar] || 1
+ @numerator = options[0][:numerator] || UNITY_ARRAY
+ @denominator = options[0][:denominator] || UNITY_ARRAY
+ @signature = options[0][:signature]
+ when Array:
+ initialize(*options[0])
+ return
when Numeric:
- @scalar = options
- @numerator = @denominator = ['<1>']
+ @scalar = options[0]
+ @numerator = @denominator = UNITY_ARRAY
when Time:
- @scalar = options.to_f
+ @scalar = options[0].to_f
@numerator = ['<second>']
- @denominator = ['<1>']
+ @denominator = UNITY_ARRAY
else
raise ArgumentError, "Invalid Unit Format"
end
self.update_base_scalar
self.replace_temperature
- self.freeze
+
+ unary_unit = self.units
+ opt_units = options[0].scan(NUMBER_REGEX)[0][1] if String === options[0]
+ unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(temp|deg)(C|K|R|F)/)
+ @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty?
+ end
+ unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /(temp|deg)(C|K|R|F)/) then
+ @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
+ end
+ @scalar.freeze
+ @numerator.freeze
+ @denominator.freeze
+ @base_scalar.freeze
+ @signature.freeze
+ @is_base.freeze
+ self
end
+
+ def kind
+ return @@KINDS[self.signature]
+ end
+ def self.cached
+ return @@cached_units
+ end
+
+ def self.base_unit_cache
+ return @@base_unit_cache
+ end
+
def to_unit
self
end
alias :unit :to_unit
# Returns 'true' if the Unit is represented in base units
def is_base?
- return true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/
+ return @is_base if defined? @is_base
+ return @is_base=true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/
n = @numerator + @denominator
- n.compact.each do |x|
- return false unless x == '<1>' ||
- (@@UNIT_VALUES[Regexp.escape(x)] &&
- @@UNIT_VALUES[Regexp.escape(x)][:denominator].nil? &&
- @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x)))
+ for x in n.compact do
+ return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x)))
end
- return true
+ return @is_base = true
end
#convert to base SI units
def to_base
return self if self.is_base?
-# return self.to('degK') if self.units =~ /temp(C|K|F|R)/
+ cached = @@base_unit_cache[self.units] * self.scalar rescue nil
+ return cached if cached
+
num = []
den = []
- q = @scalar
- @numerator.compact.each do |unit|
- if @@PREFIX_VALUES[Regexp.escape(unit)]
- q *= @@PREFIX_VALUES[Regexp.escape(unit)]
+ q = 1
+ for unit in @numerator.compact do
+ if @@PREFIX_VALUES[unit]
+ q *= @@PREFIX_VALUES[unit]
else
- q *= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] if @@UNIT_VALUES[Regexp.escape(unit)]
- num << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
- den << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
+ q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
+ num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
+ den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
end
end
- @denominator.compact.each do |unit|
- if @@PREFIX_VALUES[Regexp.escape(unit)]
- q /= @@PREFIX_VALUES[Regexp.escape(unit)]
+ for unit in @denominator.compact do
+ if @@PREFIX_VALUES[unit]
+ q /= @@PREFIX_VALUES[unit]
else
- q /= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] if @@UNIT_VALUES[Regexp.escape(unit)]
- den << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
- num << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
+ q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
+ den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
+ num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
end
end
num = num.flatten.compact
den = den.flatten.compact
- num = ['<1>'] if num.empty?
-
- Unit.new(Unit.eliminate_terms(q,num,den))
+ num = UNITY_ARRAY if num.empty?
+ base= Unit.new(Unit.eliminate_terms(q,num,den))
+ @@base_unit_cache[self.units]=base
+ return base * @scalar
end
# Generate human readable output.
# If the name of a unit is passed, the scalar will first be converted to the target unit before output.
# some named conversions are available
#
# :ft - outputs in feet and inches (e.g., 6'4")
# :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
def to_s(target_units=nil)
- case target_units
- when :ft:
- inches = self.to("in").scalar
- "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
- when :lbs:
- ounces = self.to("oz").scalar
- "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
- when String
- begin #first try a standard format string
- target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
- return self.to($2).to_s($1) if $2
- "#{($1 || '%g') % @scalar || 0} #{self.units}".strip
- rescue #if that is malformed, try a time string
- return (Time.gm(0) + self).strftime(target_units)
- end
+ out = @output[target_units] rescue nil
+ if out
+ return out
else
- "#{'%g' % @scalar} #{self.units}".strip
+ case target_units
+ when :ft:
+ inches = self.to("in").scalar
+ out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
+ when :lbs:
+ ounces = self.to("oz").scalar
+ out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
+ when String
+ begin #first try a standard format string
+ target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
+ out = $2 ? self.to($2).to_s($1) : "#{($1 || '%g') % @scalar || 0} #{self.units}".strip
+ rescue #if that is malformed, try a time string
+ out = (Time.gm(0) + self).strftime(target_units)
+ end
+ else
+ out = "#{'%g' % @scalar} #{self.units}".strip
+ end
+ @output = {target_units => out}
+ return out
end
end
def inspect(option=nil)
return super() if option == :dump
self.to_s
end
# returns true if no associated units
+ # false, even if the units are "unitless" like 'radians, each, etc'
def unitless?
- (@numerator == ['<1>'] && @denominator == ['<1>'])
+ (@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
end
# Compare two Unit objects. Throws an exception if they are not of compatible types.
# Comparisons are done based on the value of the unit in base SI units.
def <=>(other)
@@ -264,13 +396,12 @@
# Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
# throws an exception if the units are not compatible.
def +(other)
if Unit === other
if self =~ other then
- a = self.to_base
- b = other.to_base
- Unit.new(:scalar=>(a.scalar + b.scalar), :numerator=>a.numerator, :denominator=>b.denominator).to(self)
+ q = self.zero? ? 1 : (self.scalar / self.base_scalar)
+ Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
else
raise ArgumentError, "Incompatible Units"
end
elsif Time === other
other + self
@@ -283,40 +414,50 @@
# Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
# throws an exception if the units are not compatible.
def -(other)
if Unit === other
if self =~ other then
- a = self.to_base
- b = other.to_base
- Unit.new(:scalar=>(a.scalar - b.scalar), :numerator=>a.numerator, :denominator=>b.denominator).to(self)
+ q = self.zero? ? 1 : (self.scalar / self.base_scalar)
+ Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
else
raise ArgumentError, "Incompatible Units"
end
elsif Time === other
other - self
else
x,y = coerce(other)
- x - y
+ y-x
end
end
# Multiply two units.
def *(other)
- if Unit === other
- Unit.new(Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator))
+ case other
+ when Unit
+ opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)
+ opts.merge!(:signature => @signature + other.signature)
+ Unit.new(opts)
+ when Numeric
+ Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
else
x,y = coerce(other)
x * y
end
end
# Divide two units.
# Throws an exception if divisor is 0
def /(other)
- if Unit === other
+ case other
+ when Unit
raise ZeroDivisionError if other.zero?
- Unit.new(Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator))
+ opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)
+ opts.merge!(:signature=> @signature - other.signature)
+ Unit.new(opts)
+ when Numeric
+ raise ZeroDivisionError if other.zero?
+ Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
else
x,y = coerce(other)
y / x
end
end
@@ -373,20 +514,20 @@
return self.root(n.abs).inverse if n < 0
vec = self.unit_signature_vector
vec=vec.map {|x| x % n}
raise ArgumentError, "Illegal root" unless vec.max == 0
- num = @numerator.clone
- den = @denominator.clone
+ num = @numerator.dup
+ den = @denominator.dup
- @numerator.uniq.each do |item|
+ for item in @numerator.uniq do
x = num.find_all {|i| i==item}.size
r = ((x/n)*(n-1)).to_int
r.times {|x| num.delete_at(num.index(item))}
end
- @denominator.uniq.each do |item|
+ for item in @denominator.uniq do
x = den.find_all {|i| i==item}.size
r = ((x/n)*(n-1)).to_int
r.times {|x| den.delete_at(den.index(item))}
end
q = @scalar**(1/n)
@@ -417,11 +558,10 @@
return self if other.nil?
return self if TrueClass === other
return self if FalseClass === other
if (Unit === other && other.units =~ /temp(K|C|R|F)/) || (String === other && other =~ /temp(K|C|R|F)/)
raise ArgumentError, "Receiver is not a temperature unit" unless self.signature==400
- return self.to_base.to(other) unless self.is_base?
start_unit = self.units
target_unit = other.units rescue other
q=case start_unit
when 'degC':
case target_unit
@@ -450,11 +590,12 @@
when 'tempK' : @scalar*(5.0/9.0)
when 'tempF' : @scalar - 459.67
when 'tempR' : @scalar
end
else
- raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}"
+ return self.to_base.to(other) unless self.is_base?
+ #raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}"
end
target_unit =~ /temp(C|K|F|R)/
Unit.new("#{q} deg#{$1}")
else
case other
@@ -464,18 +605,18 @@
when String: target = Unit.new(other)
else
raise ArgumentError, "Unknown target units"
end
raise ArgumentError, "Incompatible Units" unless self =~ target
- one = @numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:scalar] }.compact
- two = @denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:scalar] }.compact
+ one = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
+ two = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
- one = target.numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:scalar] }.compact
- two = target.denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:scalar] }.compact
+ one = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
+ two = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
q = @scalar * v/y
- Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator)
+ Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
end
end
alias :>> :to
alias :convert_to :to
@@ -487,67 +628,70 @@
raise RuntimeError, "Can't convert to float unless unitless. Use Unit#scalar"
end
# returns the 'unit' part of the Unit object without the scalar
def units
- return "" if @numerator == ["<1>"] && @denominator == ["<1>"]
+ return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
+ return @unit_name unless @unit_name.nil?
output_n = []
output_d =[]
num = @numerator.clone.compact
den = @denominator.clone.compact
- if @numerator == ["<1>"]
+ if @numerator == UNITY_ARRAY
output_n << "1"
else
num.each_with_index do |token,index|
- if token && @@PREFIX_VALUES[Regexp.escape(token)] then
- output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(num[index+1])]}"
+ if token && @@PREFIX_VALUES[token] then
+ output_n << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[num[index+1]]}"
num[index+1]=nil
else
- output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
+ output_n << "#{@@OUTPUT_MAP[token]}" if token
end
end
end
- if @denominator == ['<1>']
+ if @denominator == UNITY_ARRAY
output_d = ['1']
else
den.each_with_index do |token,index|
- if token && @@PREFIX_VALUES[Regexp.escape(token)] then
- output_d << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(den[index+1])]}"
+ if token && @@PREFIX_VALUES[token] then
+ output_d << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[den[index+1]]}"
den[index+1]=nil
else
- output_d << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
+ output_d << "#{@@OUTPUT_MAP[token]}" if token
end
end
end
on = output_n.reject {|x| x.empty?}.map {|x| [x, output_n.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))}
od = output_d.reject {|x| x.empty?}.map {|x| [x, output_d.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))}
- "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
+ out = "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
+ @unit_name = out unless self.kind == :temperature
+ return out
end
# negates the scalar of the Unit
def -@
- Unit.new([-@scalar,@numerator,@denominator])
+ Unit.new(-@scalar,@numerator,@denominator)
end
# returns abs of scalar, without the units
def abs
return @scalar.abs
end
def ceil
- Unit.new([@scalar.ceil, @numerator, @denominator])
+ Unit.new(@scalar.ceil, @numerator, @denominator)
end
def floor
- Unit.new([@scalar.floor, @numerator, @denominator])
+ Unit.new(@scalar.floor, @numerator, @denominator)
end
# changes internal scalar to an integer, but retains the units
# if unitless, returns an int
def to_int
return @scalar.to_int if unitless?
- Unit.new([@scalar.to_int, @numerator, @denominator])
+ Unit.new(@scalar.to_int, @numerator, @denominator)
end
# Tries to make a Time object from current unit
def to_time
Time.at(self)
@@ -555,39 +699,39 @@
alias :time :to_time
alias :to_i :to_int
alias :truncate :to_int
def round
- Unit.new([@scalar.round, @numerator, @denominator])
+ Unit.new(@scalar.round, @numerator, @denominator)
end
# true if scalar is zero
def zero?
return @scalar.zero?
end
# '5 min'.unit.ago
def ago
- Time.now - self rescue DateTime.now - self
+ self.before
end
# '5 min'.before(time)
def before(time_point = ::Time.now)
raise ArgumentError, "Must specify a Time" unless time_point
if String === time_point
- time_point.time - self
+ time_point.time - self rescue time_point.datetime - self
else
- time_point - self
+ time_point - self rescue time_point.to_datetime - self
end
end
alias :before_now :before
# 'min'.since(time)
def since(time_point = ::Time.now)
case time_point
when Time: (Time.now - time_point).unit('s').to(self)
- when DateTime: (DateTime.now - time_point).unit('d').to(self)
+ when DateTime, Date: (DateTime.now - time_point).unit('d').to(self)
when String:
(DateTime.now - time_point.time(:context=>:past)).unit('d').to(self)
else
raise ArgumentError, "Must specify a Time, DateTime, or String"
end
@@ -595,11 +739,11 @@
# 'min'.until(time)
def until(time_point = ::Time.now)
case time_point
when Time: (time_point - Time.now).unit('s').to(self)
- when DateTime: (time_point - DateTime.now).unit('d').to(self)
+ when DateTime, Date: (time_point - DateTime.now).unit('d').to(self)
when String:
r = (time_point.time(:context=>:future) - DateTime.now)
Time === time_point.time ? r.unit('s').to(self) : r.unit('d').to(self)
else
raise ArgumentError, "Must specify a Time, DateTime, or String"
@@ -608,29 +752,30 @@
# '5 min'.from(time)
def from(time_point = ::Time.now)
raise ArgumentError, "Must specify a Time" unless time_point
if String === time_point
- time_point.time + self
+ time_point.time + self rescue time_point.datetime + self
else
- time_point + self
+ time_point + self rescue time_point.to_datetime + self
end
end
alias :after :from
alias :from_now :from
def succ
raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
q = @scalar.to_i.succ
- Unit.new([q, @numerator, @denominator])
+ Unit.new(q, @numerator, @denominator)
end
# Protected and Private Functions that should only be called from this class
protected
def update_base_scalar
+ return @base_scalar unless @base_scalar.nil?
if self.is_base?
@base_scalar = @scalar
@signature = unit_signature
else
base = self.to_base
@@ -651,41 +796,46 @@
# calculates the unit signature vector used by unit_signature
def unit_signature_vector
return self.to_base.unit_signature_vector unless self.is_base?
result = self
- y = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle]
- vector = Array.new(y.size,0)
- y.each_with_index do |units,index|
- vector[index] = result.numerator.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
- vector[index] -= result.denominator.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
+ vector = Array.new(SIGNATURE_VECTOR.size,0)
+ for element in @numerator
+ if r=@@ALL_UNIT_DEFINITIONS[element]
+ n = SIGNATURE_VECTOR.index(r[2])
+ vector[n] = vector[n] + 1 if n
+ end
end
+ for element in @denominator
+ if r=@@ALL_UNIT_DEFINITIONS[element]
+ n = SIGNATURE_VECTOR.index(r[2])
+ vector[n] = vector[n] - 1 if n
+ end
+ end
vector
end
def replace_temperature
- return self unless self.signature == 400 && self.units =~ /temp(R|K|F|C)/
+ return self unless self.kind == :temperature && self.units =~ /temp(R|K|F|C)/
un = $1
- target = self.units
@numerator = case un
- when 'R' : ['<rankine>']
- when 'C' : ['<celcius>']
- when 'F' : ['<farenheit>']
- when 'K' : ['<kelvin>']
+ when 'R' : RANKINE
+ when 'C' : CELCIUS
+ when 'F' : FARENHEIT
+ when 'K' : KELVIN
end
+ @unit_name = nil
r= self.to("tempK")
- @numerator = r.numerator
- @denominator = r.denominator
- @scalar = r.scalar
+ copy(r)
end
-
-
+
private
def initialize_copy(other)
- @numerator = other.numerator.clone
- @denominator = other.denominator.clone
+ @numerator = other.numerator.dup
+ @denominator = other.denominator.dup
+
end
# calculates the unit signature id for use in comparing compatible units and simplification
# the signature is based on a simple classification of units and is based on the following publication
#
@@ -693,21 +843,22 @@
# 21(8), Aug 1995, pp.651-661
# doi://10.1109/32.403789
# http://ieeexplore.ieee.org/Xplore/login.jsp?url=/iel1/32/9079/00403789.pdf?isnumber=9079&prod=JNL&arnumber=403789&arSt=651&ared=661&arAuthor=Novak%2C+G.S.%2C+Jr.
#
def unit_signature
+ return @signature unless @signature.nil?
vector = unit_signature_vector
vector.each_with_index {|item,index| vector[index] = item * 20**index}
@signature=vector.inject(0) {|sum,n| sum+n}
end
def self.eliminate_terms(q, n, d)
- num = n.clone
- den = d.clone
+ num = n.dup
+ den = d.dup
- num.delete_if {|v| v == '<1>'}
- den.delete_if {|v| v == '<1>'}
+ num.delete_if {|v| v == UNITY}
+ den.delete_if {|v| v == UNITY}
combined = Hash.new(0)
i = 0
loop do
break if i > num.size
@@ -716,11 +867,11 @@
i += 2
else
k = num[i]
i += 1
end
- combined[k] += 1 unless k.nil? || k == '<1>'
+ combined[k] += 1 unless k.nil? || k == UNITY
end
j = 0
loop do
break if j > den.size
@@ -729,23 +880,23 @@
j += 2
else
k = den[j]
j += 1
end
- combined[k] -= 1 unless k.nil? || k == '<1>'
+ combined[k] -= 1 unless k.nil? || k == UNITY
end
num = []
den = []
- combined.each do |key,value|
+ for key, value in combined do
case
when value > 0 : value.times {num << key}
when value < 0 : value.abs.times {den << key}
end
end
- num = ["<1>"] if num.empty?
- den = ["<1>"] if den.empty?
+ num = UNITY_ARRAY if num.empty?
+ den = UNITY_ARRAY if den.empty?
{:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
end
# parse a string into a unit object.
@@ -757,96 +908,105 @@
# "37 degC"
# "1" -- creates a unitless constant with value 1
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
# 6'4" -- recognized as 6 feet + 4 inches
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
- def parse(unit_string="0")
- @numerator = ['<1>']
- @denominator = ['<1>']
+ def parse(passed_unit_string="0")
+
+ unit_string = passed_unit_string.dup
+ if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
+ unit_string = "#{$1} USD"
+ end
+ if unit_string =~ /(#{SCI_NUMBER})\s*%/
+ unit_string = "#{$1} percent"
+ end
+
+ unit_string =~ NUMBER_REGEX
+ unit = @@cached_units[$2]
+ mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0
+ if unit
+ copy(unit)
+ @scalar *= mult
+ @base_scalar *= mult
+ return self
+ end
+
unit_string.gsub!(/[<>]/,"")
# Special processing for unusual unit strings
# feet -- 6'5"
- feet, inches = unit_string.scan(/(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/)[0]
+ feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
if (feet && inches)
result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
- @scalar = result.scalar
- @numerator = result.numerator
- @denominator = result.denominator
- @base_scalar = result.base_scalar
+ copy(result)
return self
end
# weight -- 8 lbs 12 oz
- pounds, oz = unit_string.scan(/(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/)[0]
+ pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
if (pounds && oz)
result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
- @scalar = result.scalar
- @numerator = result.numerator
- @denominator = result.denominator
- @base_scalar = result.base_scalar
+ copy(result)
return self
end
- @scalar, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts
+ @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts
- top.scan(/([^ \*]+)(?:\^|\*\*)([\d-]+)/).each do |item|
+ top.scan(TOP_REGEX).each do |item|
n = item[1].to_i
x = "#{item[0]} "
case
when n>=0 : top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
end
end
+ bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom
+ @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty?
+ @scalar = 1 unless @scalar.kind_of? Numeric
+
+ @numerator ||= UNITY_ARRAY
+ @denominator ||= UNITY_ARRAY
+ @numerator = top.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if top
+ @denominator = bottom.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if bottom
+ us = "#{(top || '' + bottom || '')}".to_s.gsub(@@UNIT_MATCH_REGEX,'').gsub(/[\d\*, "'_^\/\$]/,'')
+ raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized (#{us})") unless us.empty?
- bottom.gsub!(/([^* ]+)(?:\^|\*\*)(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom
- if @scalar.empty?
- if top =~ /[\dEe+.-]+/
- @scalar = top.to_f # need this for 'number only' initialization
- else
- @scalar = 1 # need this for 'unit only' intialization
- end
- else
- @scalar = @scalar.to_f
- end
-
- @numerator = top.scan(/(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/).delete_if {|x| x.empty?}.compact if top
- @denominator = bottom.scan(/(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/).delete_if {|x| x.empty?}.compact if bottom
-
@numerator = @numerator.map do |item|
- item.map {|x| Regexp.escape(x) if x}
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
end.flatten.compact.delete_if {|x| x.empty?}
@denominator = @denominator.map do |item|
- item.map {|x| Regexp.escape(x) if x}
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
end.flatten.compact.delete_if {|x| x.empty?}
- @numerator = ['<1>'] if @numerator.empty?
- @denominator = ['<1>'] if @denominator.empty?
+ @numerator = UNITY_ARRAY if @numerator.empty?
+ @denominator = UNITY_ARRAY if @denominator.empty?
self
end
end
# Allow date objects to do offsets by a time unit
# Date.today + U"1 week" => gives today+1 week
class Date
alias :unit_date_add :+
- def +(unit)
+ def +unit
case unit
- when Unit: unit_date_add(unit.to('day').scalar)
+ when Unit:
+ unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units
+ unit_date_add(unit.to('day').scalar)
when Time: unit_date_add(unit.to_datetime)
else
unit_date_add(unit)
end
end
alias :unit_date_sub :-
- def -(unit)
+ def -unit
case unit
- when Unit: unit_date_sub(unit.to('day').scalar)
+ when Unit:
+ unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units
+ unit_date_sub(unit.to('day').scalar)
when Time: unit_date_sub(unit.to_datetime)
else
unit_date_sub(unit)
end
end
@@ -862,21 +1022,22 @@
end
end
class Object
- def Unit(other)
+ def Unit(*other)
other.to_unit
end
+
alias :U :Unit
alias :u :Unit
end
# make a unitless unit with a given scalar
class Numeric
def to_unit(other = nil)
- other ? Unit.new(self) * Unit.new(other) : Unit.new(self)
+ other ? Unit.new(self, other) : Unit.new(self)
end
alias :unit :to_unit
alias :u :to_unit
end
@@ -891,11 +1052,11 @@
end
# make a string into a unit
class String
def to_unit(other = nil)
- other ? Unit.new(self) >> other : Unit.new(self)
+ other ? Unit.new(self).to(other) : Unit.new(self)
end
alias :unit :to_unit
alias :u :to_unit
alias :unit_format :%
@@ -959,10 +1120,20 @@
r=DateTime.civil(*ParseDate.parsedate(self)[0..5].compact)
end
raise RuntimeError, "Invalid Time String" if r == DateTime.new
return r
end
+
+ def to_date(options={})
+ begin
+ r = Chronic.parse(self,options).to_date
+ rescue
+ r = Date.civil(*ParseDate.parsedate(self)[0..5].compact)
+ end
+ raise RuntimeError, 'Invalid Date String' if r == Date.new
+ return r
+ end
def datetime(options = {})
self.to_datetime(options) rescue self.to_time(options)
end
end
@@ -992,10 +1163,14 @@
def to_datetime
DateTime.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400
end
+ def to_date
+ Date.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400
+ end
+
def +(other)
case other
when Unit: unit_add(other.to('s').scalar)
when DateTime: unit_add(other.to_time)
else
@@ -1083,6 +1258,8 @@
module_function :cosh
module_function :unit_tan
module_function :tan
module_function :unit_tanh
module_function :tanh
-end
\ No newline at end of file
+end
+
+Unit.setup