require 'mathn' require 'rational' require 'date' require 'parsedate' # = Ruby Units 0.2.1 # # Copyright 2006 by Kevin C. Olbrich, Ph.D. # # See http://rubyforge.org/ruby-units/ # # http://www.sciwerks.org # # mailto://kevin.olbrich+ruby-units@gmail.com # # See README for detailed usage instructions and examples # # ==Unit Definition Format # # '' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{ } , %w{ } ], # # Prefixes (e.g., a :prefix classification) get special handling # Note: The accuracy of unit conversions depends on the precision of the conversion factor. # If you have more accurate estimates for particular conversion factors, please send them # to me and I will incorporate them into the next release. It is also incumbent on the end-user # to ensure that the accuracy of any conversions is sufficient for their intended application. # # While there are a large number of unit specified in the base package, # there are also a large number of units that are not included. # This package covers nearly all SI, Imperial, and units commonly used # in the United States. If your favorite units are not listed here, send me an email # # To add / override a unit definition, add a code block like this.. # # class Unit < Numeric # UNIT_DEFINITIONS = { # ' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{ } , %w{ } ] # } # end # Unit.setup class Unit < Numeric require 'units' # pre-generate hashes from unit definitions for performance. @@USER_DEFINITIONS = {} @@PREFIX_VALUES = {} @@PREFIX_MAP = {} @@UNIT_MAP = {} @@UNIT_VALUES = {} @@OUTPUT_MAP = {} @@UNIT_VECTORS = {} def self.setup (UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value| 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)][: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)] end @@OUTPUT_MAP[Regexp.escape(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('|') end self.setup include Comparable 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 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) when Hash: @scalar = options[:scalar] || 1 @numerator = options[:numerator] || ["<1>"] @denominator = options[:denominator] || [] when Array: parse("#{options[0]} #{options[1]}/#{options[2]}") when Numeric: @scalar = options @numerator = @denominator = ['<1>'] when Time: @scalar = options.to_f @numerator = [''] @denominator = ['<1>'] else raise ArgumentError, "Invalid Unit Format" end 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.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 = @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)][: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.compact.each do |unit| if @@PREFIX_VALUES[Regexp.escape(unit)] q /= @@PREFIX_VALUES[Regexp.escape(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] end end num = num.flatten.compact den = den.flatten.compact num = ['<1>'] if num.empty? Unit.new(Unit.eliminate_terms(q,num,den)) 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 else "#{'%g' % @scalar} #{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) 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 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.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.scalar == other.scalar) && (self.units == other.units) else x,y = coerce(other) x === y end end 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 = @scalar + other.to(self).scalar Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator) else raise ArgumentError, "Incompatible Units" end elsif Time === other other + self else x,y = coerce(other) y + x end end # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately # throws an exception if the units are not compatible. def -(other) if Unit === other if self =~ other then q = @scalar - other.to(self).scalar Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator) else raise ArgumentError, "Incompatible Units" end elsif Time === other other + self else x,y = coerce(other) x - y end end # Multiply two units. def *(other) if Unit === other 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 0 def /(other) if Unit === other 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 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) 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(:scalar=>q,:numerator=>num,:denominator=>den) end # returns inverse of Unit (1/unit) def inverse Unit("1") / self end # convert to a specified unit string or to the same units as another Unit # # unit >> "kg" will covert to kilograms # unit1 >> unit2 converts to same units as unit2 object # # To convert a Unit object to match another Unit object, use: # unit1 >>= unit2 # Throws an exception if the requested target units are incompatible with current Unit. # # Special handling for temperature conversions is supported. If the Unit object is converted # from one temperature unit to another, the proper temperature offsets will be used. # Supports Kelvin, Celcius, Farenheit, and Rankine scales. # # Note that if temperature is part of a compound unit, the temperature will be treated as a differential # and the units will be scaled appropriately. def to(other) return self if other.nil? return self if TrueClass === other return self if FalseClass === other if 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?(''): 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?( ''): 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?( ''): 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?( ''): 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 #{self.numerator}" end Unit.new("#{q} deg#{$1}") else case other when Unit: return self if other.units == self.units target = other when String: target = Unit.new(other) else raise ArgumentError, "Unknown target units" end 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)][: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 = @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 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 end vector 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 # # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, # 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 vector = unit_signature_vector 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 # 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 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 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 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? {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact} end # converts the unit back to a float if it is unitless def to_f 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 scalar def units return "" if @numerator == ["<1>"] && @denominator == ["<1>"] 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(num[index+1])]}" num[index+1]=nil else output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token end end end 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 scalar of the Unit def -@ 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]) end def floor 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]) end # Tries to make a Time object from current unit def to_time Time.at(self) end alias :time :to_time alias :to_i :to_int alias :truncate :to_int def round 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 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.local(*ParseDate.parsedate(time_point))-self else time_point - self end end # '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 String: (Time.now - Time.local(*ParseDate.parsedate(time_point))).unit('s').to(self) else raise ArgumentError, "Must specify a Time" unless time_point end end # 'min'.until(time) def until(time_point = ::Time.now) case time_point when Time: (time_point - Time.now).unit('s').to(self) when DateTime: (time_point - DateTime.now).unit('d').to(self) when String: (Time.local(*ParseDate.parsedate(time_point)) - Time.now).unit('s').to(self) else raise ArgumentError, "Must specify a Time" unless time_point end end # '5 min'.from_now def from_now self.from end # '5 min'.from(time) def from(time_point = ::Time.now) raise ArgumentError, "Must specify a Time" unless time_point if String === time_point Time.local(*ParseDate.parsedate(time_point))+self else time_point + self end end alias :after :from 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] else [Unit.new(other), self] end end private # parse a string into a unit object. # Typical formats like : # "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 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] 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 return self end # weight -- 8 lbs 12 oz 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") @scalar = result.scalar @numerator = result.numerator @denominator = result.denominator @base_scalar = result.base_scalar return self end @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!(/#{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 @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? self end end # Need the 'Uncertain' gem for this to do anything helpful if defined? Uncertain class Uncertain def to_unit(other=nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit 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) if Unit === unit unit_date_add(unit.to('day').scalar) else unit_date_add(unit) end end alias :unit_date_sub :- def -(unit) if Unit === unit unit_date_sub(unit.to('day').scalar) else unit_date_sub(unit) end end end class Object 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) end alias :unit :to_unit alias :u :to_unit end # make a unit from an array # [1, 'mm'].unit => 1 mm class Array def to_unit(other = nil) other ? Unit.new(self).to(other) : Unit.new(self) end alias :unit :to_unit alias :u :to_unit end # make a string into a unit class String def to_unit(other = nil) other ? Unit.new(self) >> other : Unit.new(self) end alias :unit :to_unit alias :u :to_unit alias :unit_format :% # format unit output using formating codes '%0.2f' % '1 mm'.unit => '1.00 mm' def %(*args) if Unit === args[0] args[0].to_s(self) else unit_format(*args) end end def from(time_point = ::Time.now) self.unit.from(time_point) end alias :after :from alias :from_now :from def ago self.unit.ago end def before(time_point = ::Time.now) self.unit.before(time_point) end alias :before_now :before def since(time_point = ::Time.now) self.unit.since(time_point) end def until(time_point = ::Time.now) self.unit.until(time_point) end def to(other) self.unit.to(other) end end # Allow time objects to use 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} s").to(other) : Unit.new("#{self.to_f} s") end alias :unit :to_unit alias :u :to_unit alias :unit_add :+ def +(other) if Unit === other unit_add(other.to('s').scalar) else unit_add(other) end end alias :unit_sub :- def -(other) if Unit === other unit_sub(other.to('s').scalar) else unit_sub(other) end end end module Math alias unit_sin sin def sin(n) if Unit === n unit_sin(n.to('radian').scalar) else unit_sin(n) end end alias unit_cos cos def cos(n) if Unit === n unit_cos(n.to('radian').scalar) else unit_cos(n) end end alias unit_sinh sinh def sinh(n) if Unit === n unit_sinh(n.to('radian').scalar) else unit_sinh(n) end end alias unit_cosh cosh def cosh(n) if Unit === n unit_cosh(n.to('radian').scalar) else unit_cosh(n) end end alias unit_tan tan def tan(n) if Unit === n unit_tan(n.to('radian').scalar) else unit_tan(n) end end alias unit_tanh tanh def tanh(n) if Unit === n unit_tanh(n.to('radian').scalar) else unit_tanh(n) end end module_function :unit_sin module_function :sin module_function :unit_cos module_function :cos module_function :unit_sinh module_function :sinh module_function :unit_cosh module_function :cosh module_function :unit_tan module_function :tan module_function :unit_tanh module_function :tanh end