require 'eymiha/units/units' require 'set' module Eymiha # A NumericWithUnits is the intersection of a Numeric and a UnitsHash. # # Unit-sensitive coding is made much easier using an object that transparently # adds units to a common everyday Numerics. Everything that can be done to a # Numeric is still there, but explicit and implicit unit conversions are # present in all of the operations. # # With this, numbers with units can be created easily. For example, # # 2.feet # a length of 2 feet # 5.inches^2 # an area of 5 square inches # 44.5.ft/sec # a velocity of 44.5 feet per second # # This should provide a good starting point for using the Units framework. # Also pay attention to the examples given in the method documentation; some # of the dynamic features of the framework are exposed in them. class NumericWithUnits include Comparable @@debug = false def self.debug=(value) @@debug = value end # A Numeric containing the numeric part of the instance attr_accessor :numeric # A UnitsHash containing the units part of the instance attr_accessor :unit attr_accessor :original # :nodoc: # Returns a new NumericWithUnits instance whose numeric part is set to # numeric and whose units part is set to a units hash for the unit raised # to the provided power. def initialize(numeric,unit,power=1) @numeric, @unit = numeric, units_hash(unit)**power end def units_hash(unit) # :nodoc: (unit.kind_of? UnitsHash) ? unit : UnitsHash.new(unit) end # Returns a String representation of the instance, using the named format # if provided. # # 15.5.minutes.to_s # 15.5 minutes # 15.5.minutes.to_seconds.to_s # 930.0 seconds # 15.5.minutes.in_seconds.to_s # 930.0 seconds # 15.5.minutes.seconds.to_s # 930.0 seconds # (15.5.minutes+1).seconds.to_s # 990.0 seconds # (15.5.minutes.seconds+1).to_s # 931.0 seconds # 10.feet_per_minute.to_s # 10 ft / min # seconds_per_hour.to_s # 3600.0 # 14.5.inches.to_s(:feet_inches_and_32s) # "1 foot 2-16/32 inches" def to_s(format = nil) format == nil ? "#{numeric} #{unit.to_s(numeric)}" : self.format(format) end def promote_original # :nodoc: @numeric, @unit = original.numeric, original.unit end # Compares the numeric and units parts of the instance with the value. If # the value is a Numeric, the units are assumed to match. If the # UnitsMeasures don't match, a UnitsException is raised. # # 2.ft <=> 1.yd # -1 # 3.ft <=> 1.yd # 0 # 4.ft <=> 1.yd # 1 # 4.ft <=> 3.5 # 1 # 4.ft <=> 2.minutes # UnitsException def <=>(value) if derived? reduce <=> value elsif value.kind_of? NumericWithUnits if value.derived? self <=> value.reduce else align(value).numeric <=> value.numeric end elsif value.kind_of? Numeric numeric <=> value else raise UnitsException.new("units mismatch") end end # Returns true if the value and the instance are within a distance epsilon # of each other. If the value is a Numeric, the units are assumed to match. # If the UnitsMeasures don't match, a UnitsException is raised. # # 1.ft.approximately_equals? 0.33.yd # false # 1.ft.approximately_equals? 0.333333.yd # true def approximately_equals?(value,epsilon=Numeric.epsilon) if derived? reduce.approximately_equals?(value,epsilon) elsif value.kind_of? NumericWithUnits if value.derived? approximately_equals?(value.reduce,epsilon) else align(value).numeric.approximately_equals?(value.numeric,epsilon) end elsif value.kind_of? Numeric numeric.approximately_equals?(value,epsilon) else raise UnitsException.new("units mismatch") end end alias :=~ :approximately_equals? # Unary plus returns a copy of the instance. def +@ clone end # Unary minus returns a copy of the instance with its numeric part negated. def -@ value = clone value.numeric = -(value.numeric) value end # Returns a new NumericWithUnits containing the sum of the instance and # the value. If the value is a Numeric, the units are assumed to match. # If the UnitsMeasures don't match, a UnitsException is raised. # # 6.inches + 1.foot # 18 inches # 6.inches + 1 # 7 inches # 6.inches + 1.sec # UnitsException def +(value) if derived? reduce+value elsif value.kind_of? NumericWithUnits if value.derived? self+value.reduce else aligned_value = align(value) aligned_value.numeric += value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(numeric+value,unit) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the difference between the # instance and the value. If the value is a Numeric, the units are assumed # to match. If the UnitsMeasures don't match, a UnitsException is raised. # # 6.inches - 1.foot # -6 inches # 6.inches - 1 # 5 inches # 6.inches - 1.sec # UnitsException def -(value) self + (-value) end # Returns a new NumericWithUnits containing the product of the instance and # the value. The units of the two factors are merged. If the value is not a # Numeric nor NumericWithUnits, a UnitsException is thrown. # # 6.in * 2.ft # 1 foot^2 # 6.in * 2 # 12 inches # 6.in * 2.sec # 12 ft sec # 6.in * "hello" # UnitsException def *(value) if derived? reduce*value elsif value.kind_of? NumericWithUnits if value.derived? self*value.reduce else extend(value,1) end elsif value.kind_of? Numeric NumericWithUnits.new(numeric*value,unit) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the value of the instance # divided by the value. The units of the result are the units of the # instance merged with the recipricol of the units of the value. If the # value is neither a Numeric nor NumericWithUnits, a UnitsException is # thrown. # # 6.in / 2.ft # 0.25 # 6.in / 2 # 3 inches # 6.in / 2.sec # 3 ft / sec # 6.in / "hello" # UnitsException def /(value) if derived? reduce/value elsif value.kind_of? NumericWithUnits if value.derived? self/value.reduce else extend(value,-1) end elsif value.kind_of? Numeric NumericWithUnits.new(numeric/value,unit) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the numeric and units parts of # the instance both raised to the valueth power. If the value is not # a Numeric, a UnitsException is thrown. # # 6.in ** 3 # 216 in^3 # 6.in ** 3.in # UnitsException def **(value) if (value.kind_of? Numeric) && !(value.kind_of? NumericWithUnits) extend(nil,value) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the numeric and units parts of # the instance with just the units raised to the valueth power. If the value # is not a Numeric, a UnitsException is thrown. # # 6.in ^ 3 # 6 in^3 # 6.in ^ 3.in # UnitsException def ^(value) if (value.kind_of? Numeric) && !(value.kind_of? NumericWithUnits) NumericWithUnits.new(numeric,unit,value) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the numeric part of the instance # modulo the numeric part of the value, with the units part equal to the # instance's units part. If the value is a Numeric, the units are assumed # to match. If the UnitsMeasures don't match, a UnitsException is raised. # # 30.in % 2.ft # 0.5 feet # 5.in % 2.3 # 0.4 inches # 5.in % 2.sec # UnitsException def %(value) if derived? reduce%value elsif value.kind_of? NumericWithUnits if value.derived? self%value.reduce else aligned_value = align(value) aligned_value.numeric = aligned_value.numeric % value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(numeric % value,unit) else raise UnitsException.new("units mismatch") end end # Returns a new NumericWithUnits containing the numeric part of the value # modulo the numeric part of the instance, with the units part equal to the # instance's units part. If the value is a Numeric, the units are assumed # to match. If the UnitsMeasures don't match, a UnitsException is raised. # # 30.in % 2.ft # 24 inches # 5.in % 2.3 # 2.3 inches # 5.in % 2.sec # UnitsException def inv_mod(value) if derived? reduce.inv_mod value elsif value.kind_of? NumericWithUnits if value.derived? self.inv_mod value.reduce else aligned_value = align(value) aligned_value.numeric = value.numeric % aligned_value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(value % numeric,unit) else raise UnitsException.new("units mismatch") end end def can_align(target,exact=true) # :nodoc: ru, tru = reduce.unit, target.reduce.unit !exact || (ru == tru) end def reduce_power(p,f,type) # :nodoc: if type == :whole_powers pa = p.abs (p == 0) ? [p,f,0] : (pa < f) ? [p, f, pa/p] : [p, f, p/f] else (p == 0) ? [p,f,0] : [p, f, (1.0*p)/f] end end def common_power(ps) # :nodoc: ps.values.collect {|a| a[2]}.inject {|p,e| p.abs < e.abs ? p : e} end def revise_power(rp,p) # :nodoc: [rp[0], rp[1], p, rp[0]-p*rp[1]] end # Sets and returns the type of exponentiation merging during alignment. def self.derived_align_type=(type) @@derived_align_type = type end # Returns the type of exponentiation merging during alignment. # * :whole_powers require exponents to have integer values # * :fractional_powers allow exponents to have rational values def self.derived_align_type @@derived_align_type end @@derived_align_type = :whole_powers # Returns a new NumericWithUnits whose value is equivalent to that of the # instance, but whose units are aligned to the target, according to the # value of derived_align_type. If all is true, then the UnitsMeasure of the # instance and the target must match exactly, or else a UnitsException is # raised. # # (80.miles_per_hour).align(1.min,false) # 1.3333333 mi / min def align(target,all=true,type=@@derived_align_type) if target.kind_of? Array piece_align(target) elsif !derived? && !target.derived? simple_align(target,all) else power_align(target,all,type) end end def simple_align(target,all=true) # :nodoc: puts "simple align #{self} #{target}" if @@debug factor = 1 target_unit = UnitsHash.new unit.each do |tu,tv| su = target.unit.keys.select {|u| u.units_measure.equal? tu.units_measure } if tu.equals.kind_of? Array m = tu.equals.select{|u| u.unit[su[0]]} m = tu.equals.collect{|u| u.align(1.unite(su[0]))}.compact if m.size == 0 factor *= (m[0].numeric)**tv target_unit[su[0]] = tv else if su.size == 1 e = su[0].equals if e.kind_of? Array m = e.select{|u| u.unit[tu]} m = e.equals.collect{|u| u.align(1.unite(su[0]))}.compact if m.size == 0 factor *= (1.0/m[0].numeric)**tv else factor *= (1.0*tu.equals.numeric/e.numeric)**tv end target_unit[su[0]] = tv else target_unit[tu] = tv end end end raise UnitsException.new("units mismatch") if all && (target.unit != target_unit) nwu = NumericWithUnits.new(numeric*factor,target_unit) puts " simple_align returning #{nwu}" if @@debug nwu end def power_align(target,all=true,type=@@derived_align_type) # :nodoc: puts "power_align #{self} #{target}" if @@debug raise UnitsException.new("units mismatch") unless can_align(target,all) factor = 1 target_unit = UnitsHash.new unit_reduce = reduce puts " unit_reduce #{unit_reduce}" if @@debug target.unit.each do |tu,tv| t_u = {} tt_u = {} tu_reduce = tu.equals.reduce found = tu_reduce.unit.each do |ttu,ttv| su = unit_reduce.unit.keys.select {|u| u.units_measure.equal? ttu.units_measure } break false if su.size == 0 tt_u[ttu] = reduce_power(unit_reduce.unit[ttu],ttv,type) end if found cp = common_power(tt_u) puts " cp #{cp}" if @@debug tt_u.each {|k,v| tt_u[k] = revise_power(v,cp)} t_u[tu] = tv**cp t_factor = tu_reduce.numeric**cp puts " t_factor #{t_factor}" if @@debug target_unit[tu] = cp puts " tu.equals.numeric**cp #{tu.equals.numeric**cp}" if @@debug factor *= t_factor/(tu.equals.numeric**cp) puts " factor #{factor}" if @@debug tt_u.each {|k,v| unit_reduce.numeric /= t_factor unit_reduce.unit[k] = v[3]} end end puts " unit_reduce #{unit_reduce}" if @@debug unit_redux = unit_reduce.simple_align(self,false) puts " unit_redux #{unit_redux}" if @@debug result_unit = target_unit.merge(unit_redux) raise UnitsException.new("units mismatch") if all && (target.unit != result_unit) nwu = NumericWithUnits.new(factor*unit_redux.numeric,result_unit) puts " power_align returning #{nwu}" if @@debug nwu end def piece_align(pieces,type=@@derived_align_type) # :nodoc: puts "piece_align #{self} #{pieces}" if @@debug factor = 1 target_unit = UnitsHash.new unit_reduce = reduce pieces.each do |p| p.unit.each do |tu,tv| t_u = {} tt_u = {} tu_reduce = tu.equals.reduce found = tu_reduce.unit.each do |ttu,ttv| su = unit_reduce.unit.keys.select {|u| u.units_measure.equal? ttu.units_measure } break false if su.size == 0 tt_u[ttu] = reduce_power(unit_reduce.unit[ttu],ttv,type) end if found tt_u.each {|k,v| tt_u[k] = revise_power(v,tv)} t_u[tu] = tv t_factor = tu_reduce.numeric**tv target_unit[tu] = tv factor *= t_factor/(tu.equals.numeric**tv) tt_u.each {|k,v| unit_reduce.numeric /= t_factor unit_reduce.unit[k] = v[3]} end end end raise UnitsException.new("units mismatch") if unit_reduce.unit.has_units? nwu = NumericWithUnits.new(factor*unit_reduce.numeric,target_unit) puts " piece_align returning #{nwu}" if @@debug nwu end # Returns a new NumericWithUnits whose value is extended by raising it to # the power, or by multiplying it by units raised to the power. # # 7.miles.extend(nil,2) # 49 mi^2 # 5.feet.extend(10.ft,2) # 500 ft^3 def extend(units,power) if !units NumericWithUnits.new(numeric**power,unit**power) else value = align(units,false) extended_numeric = value.numeric*(units.numeric**power) extended_unit = value.unit.merge(units,power) (extended_unit.size == 0)? extended_numeric : NumericWithUnits.new(extended_numeric,extended_unit) end end def NumericWithUnits.commutative_operator(op,old,calc) # :nodoc: ([] << "alias :old_#{old} :#{op}" << "def #{op}(value)" << " (value.kind_of? NumericWithUnits) ? #{calc} : old_#{old}(value)" << "end"). join("\r\n") end def NumericWithUnits.create_commutative_operators(klasses) # :nodoc: commutative_operators ||= ([] << commutative_operator( "*", "multiply", "value * self" ) << commutative_operator( "/", "divide", "(value**-1) * self" ) << commutative_operator( "+", "add", "value + self" ) << commutative_operator( "-", "subtract", "(-value) + self" ) << commutative_operator( "%", "modulo", "value.inv_mod(self)" ) << commutative_operator( "<=>", "compare", "-(value <=> self)" ) << commutative_operator( ">", "gt", "(value < self)" ) << commutative_operator( "<", "lt", "(value > self)" ) << commutative_operator( ">=", "gteq", "(value <= self)" ) << commutative_operator( "<=", "lteq", "(value >= self)" ) << commutative_operator( "==", "eq", "(value == self)" ) << commutative_operator( "=~", "approxeq", "(value =~ self)" )). join("\r\n") klasses.each { |klass| klass.class_eval commutative_operators } end def method_missing(method,*args) # :nodoc: begin s = method.to_s ms = s.split '_' if ms[0] == 'to' convert! s.gsub(/^to_/,"") elsif ms[0] == 'in' convert s.gsub(/^in_/,"") elsif ms.select{|e| e == 'per'}.size > 0 convert_per method else convert s end rescue Exception value = numeric.send(method,*args) if (value.kind_of? String) "#{value} #{unit.to_s(numeric)}" else NumericWithUnits.new(value,unit) end end end def convert_per method # :nodoc: ps = method.to_s.split '_per_' raise UnitsException.new('invalid per method') if ps.size != 2 numerator = 1.unite(ps[0]) numerator_units = Set.new numerator.unit.keys denominator = 1.unite(ps[1]) denominator_units = Set.new denominator.unit.keys positives = unit.keys.collect{|k| unit[k] > 0 ? k : nil}.compact! negatives = unit.keys.collect{|k| unit[k] < 0 ? k : nil}.compact! numerator_positives = Set.new 1.unite(positives).align(numerator,false).unit.keys numerator_negatives = Set.new 1.unite(negatives).align(numerator,false).unit.keys denominator_positives = Set.new 1.unite(positives).align(denominator,false).unit.keys denominator_negatives = Set.new 1.unite(negatives).align(denominator,false).unit.keys if (numerator_units == numerator_positives) && (denominator_units == denominator_negatives) convert(ps[0]+'_and_'+ps[1]) elsif (numerator_units == numerator_negatives) && (denominator_units == denominator_positives) convert(ps[0]+'_and_'+ps[1])**-1 else raise UnitsException.new('invalid per units') end end alias :old_kind_of? :kind_of? # Returns true if the instance or it's numeric part is a kind of klass. # # 28.feet.kind_of? String # false # 28.feet.kind_of? NumericWithUnits # true # 28.feet.kind_of? Numeric # true # 28.feet.kind_of? Integer # true # 28.feet.kind_of? Float # false # 28.0.feet.kind_of? Float # true # # Note while NumericWithUnits actually descends from Object, it acts as if # it is inherited from the class of the numeric part of the instance, since # it forwards any unknown method calls to it. In this way the duck really # is a duck. def kind_of?(klass) (numeric.kind_of? klass)? true : old_kind_of?(klass) end # Returns a new NumericWithUnits whose numeric part is the target of the # Numeric's unite method. # # 28.ft^2.unite("seconds") # 28 seconds def unite(target_unit=nil,power=1,measure=nil) numeric.unite(target_unit,power,measure) end # Returns a copy of the instance converted to the target_units. Note that # the conversion is only with respect to the UnitsMeasures of the # target_units - the remainder of the units will remain unconverted. def convert(target_units=nil) target_units ? align(1.unite(target_units),false) : clone end # Converts the instance itself. def convert!(target_units=nil) result = convert(target_units) self.numeric, self.unit = result.numeric, result.unit self end # Returns the UnitsMeasures in the units part of the instance. def measure unit.measure end # Returns a String formatted using the named format defined in the # instance's measure. Raises a UnitsException if either the unit part of # the instance has no defined UnitsMeasure or a format with the given name # does not exist in that UnitsMeasure. def format(name=nil) if name == nil to_s else raise UnitsException.new("system not explicit") if (measure == nil) format = measure.formats[name] raise UnitsException.new("missing format") if format == nil format.call(self) end end # Returns true if a component of unit part of the instance has a derived # UnitsMeasure. def derived? unit.derived? end # Return a new NumericWithUnits that is equivalent to the instance but whose # unit contains no derived UnitMeasures. def reduce puts "reduce unit.reduce #{unit.reduce}" if @@debug numeric * unit.reduce end end NumericWithUnits.create_commutative_operators [ Fixnum, Bignum, Float ] end