require 'eymiha' require 'eymiha/util/forward_referencing' require 'eymiha/units/units_measure' require 'eymiha/units/units_exception' require 'eymiha/units/numeric' require 'eymiha/units/numeric_with_units' require 'eymiha/units/units_hash' require 'eymiha/units/object' module Eymiha # The Units framework # # Units is the top level that you'll typically never have to deal with # directly. While it provides a few chunks of general functionality such # as getting defined units and ranking, most of its guts are devoted to # defining new UnitsMeasures - unless you're setting up your own sets of # specialized units, you will hardly know it's here. class Units @@debug = false def self.debug=(value) @@debug = value end extend ForwardReferencing start_forward_referencing @@measures = {} @@units = {} @@defining = nil @@holding = nil # Returns an Array of the defined UnitsMeasures. def Units.units_measures @@measures.keys end # Creates or extends a UnitsMeasure. def Units.create(name, &block) measure = (@@measures[name.to_s] ||= UnitsMeasure.new) block.call measure if block_given? measure end # Creates or extends a UnitsMeasure derived from other UnitsMeasures. def Units.derive(name, target, &block) measure = ( @@measures[name.to_s] = if target.kind_of? UnitsMeasure if target.derived && (derived = find_by_derivation target.derived) derived else target end else Units.create(target.to_s) end ) block.call measure if block_given? measure end # Returns the UnitsMeasure with the given derivation. def Units.find_by_derivation(derivation) matches = @@measures.values.uniq.select { |measure| if measure.derived measure == derivation elsif derivation.size == 1 a = derivation.to_a[0] a[0] == measure and a[1] == 1 else false end } case matches.size when 0 then nil when 1 then matches[0] else raise UnitsException, "Multiple UnitsMeasures with same derivation found" end end # Removes the named UnitsMeasure from the Units framework. def Units.delete(name) @@measures.delete name.to_s end # Clears the Units framework of all defined elements. def Units.clear @@measures.clear @@units.clear forward_references_clear self end # Returns the number of defined UnitsMeasures. def Units.size @@measures.size end # Returns the named UnitsMeasure. def Units.[](name) @@measures[name.to_s] end def Units.method_missing(method,*args) # :nodoc: measure = self[method] raise UnitsException.new("UnitsMeasure '#{method}' undefined") if !measure measure end # Returns an Array containing the names of a given UnitsMeasure. def Units.names_of(units_measure) @@measures.keys.select { |name| @@measures[name].equal? units_measure } end def Units.add_unit(unit,unit_identifier=nil) # :nodoc: if unit_identifier if (element = @@units[unit_identifier]) @@units[unit_identifier] += [ unit ] unless element.index(unit) else @@units[unit_identifier] = [ unit ] end else add_unit unit, unit.name add_unit unit, unit.plural unit.abbrevs.each { |abbrev| add_unit unit, abbrev } end end # Returns an Array of UnitsUnit associated with a singular, plural or # abbreviated name. def Units.lookup(unit_identifier) @@units[unit_identifier.to_s] || [ ] end # Answers the question of whether a UnitsMeasure is being defined with the # instance if true, or nil if false. def Units.defining? @@defining end def Units.defining(measure) # :nodoc: @@defining = measure end def Units.convert(numeric,unit_identifier) # :nodoc: puts "Units:convert #{numeric} #{unit_identifier}" if @@debug if (candidates = lookup(unit_identifier)).size == 0 puts @@units.keys.sort.join(" ") if @@debug puts " no candidates!" if @@debug raise MissingUnitsException.new(unit_identifier.to_s) elsif !defining? if candidates.size > 1 raise AmbiguousUnitsException.new(unit_identifier.to_s) else unit = candidates[0] NumericWithUnits.new(numeric,unit) end else if candidates.size == 1 units = candidates else units = candidates.select { |candidate| @@defining == candidate.units_system.units_measure } units = candidates.select { |candidate| @@defining.derived[candidate.units_measure] } if units.size == 0 end case units.size when 0 then raise MissingUnitsException.new(unit_identifier.to_s) when 1 then unit = units[0] if unit.equals.kind_of? Array element = unit.equals[0] value = NumericWithUnits. new(numeric*element.numeric,element.unit) else value = NumericWithUnits. new(numeric*unit.equals.numeric,unit.equals.unit) end value.original = numeric.unite(unit) value else raise AmbiguousUnitsException.new(unit_identifier.to_s) end end end def Units.make_forward_reference(method,context) # :nodoc: @@holding ? nil : create_forward_reference(method,context) end def Units.release_forward_reference(reference = nil) # :nodoc: remove_forward_reference(reference) if reference != nil end def Units.hold_forward_reference(hold = true) # :nodoc: @@holding = hold end def Units.holding_forward_reference? # :nodoc: @@holding end def Units.establish_forward_reference_context(context) # :nodoc: defining context end # Given a Hash of units to raking weights, a set of samples, and # optionally a block that returns rankings, returns an Array of units # ordered by best weighted fit over the samples. Like golf, low scores # rank best. In absence of a block, ranking is based on the number of # unit values whose magnitudes are between 1 and 10. def Units.rank(unit_choices = {},samples = [],&numeric_ranker) # :yields: n if block_given? scores = {} samples.each {|s| unit_choices.each {|uc,w| scores[uc] = (scores[uc] || 0) + w*(yield s.convert(uc).numeric) } } scores.sort {|e1,e2| -(e1[1] <=> e2[1])}.collect {|s| s[0]} else rank(unit_choices,samples) { |n| e = ((((n.abs)-5.5).abs-4.5).at_least(0)) (e == 0) ? 0 : (e == 1)? 0 : -(e + 1/(1-e)) } end end end end