# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details. # :title: Class: AMEE::DataAbstraction::Term require 'bigdecimal' module AMEE module DataAbstraction # Base class for representing quantities which are inputs to, outputs of, or # metadata associated with, calculations. Typically several instances of the # Term class (or subclasses) will be associated with instances of the # Calculation class or its subclasses (PrototypeCalculation, # OngoingCalculation). # # Instances of Term are represented by several primary attributes: # # label:: Symbol representing the unique, machine-readable name for the # term (required) # # value:: In principle, any object which represent the desired value # which the term represents # # name:: String representing a human-readable name for the term # # path:: String representing the AMEE platform path to the AMEE item # value definition which is associated with self. # This attribute is required only if the term represents an # item value definition in the AMEE platform # # Other available attribute-like methods include type, # interface, note, unit, per_unit, # default_unit, default_per_unit and parent. # # Subclasses of the Term correspond to: # * Input # * * Profile -- corresponds to an AMEE profile item value # * * Drill -- corresponds to an AMEE drill down choice # * * Usage -- corresponds to a (runtime adjustable) AMEE usage choice # * * Metadatum -- represents other arbitrary inputs # * Output -- corresponds to an AMEE return value # class Term public # Symbol representing the unique (within the parent calculation), machine- # readable name for self. Set a value by passing an argument. # Retrieve a value by calling without an argument, e.g., # # my_term.label :distance # # my_term.label #=> :distance # attr_property :label # String representing a human-readable name for self. Set a value # by passing an argument. Retrieve a value by calling without an argument, # e.g., # # my_term.name 'Distance driven' # # my_term.name #=> 'Distance driven' # attr_property :name # Symbol representing the class the value should be parsed to. If # omitted a string is assumed, e.g.: # # my_term.type :integer # my_term.value "12" # my_term.value # => 12 # my_term.value_before_cast # => "12" # attr_property :type # String representing a the AMEE platform path for self. Set a # value by passing an argument. Retrieve a value by calling without an # argument, e.g., # # my_term.path 'mass' # # my_term.path #=> 'mass' # attr_property :path # Symbol representing the owning parent calculation of self. Set # the owning calculation object by passing as an argument. Retrieve it by # calling without an argument, e.g., # # my_calculation = # # my_term.parent my_calculation # # my_term.parent #=> # attr_accessor :parent # Stores pre-cast value attr_accessor :value_before_cast # Initialize a new instance of Term. # # The term can be configured in place by passing a block (evaluated in the # context of the new instance) which defines the term properties using the # macro-style instance helper methods. # # my_term = Term.new { # # label :size # path "vehicleSize" # hide! # ... # } # # The parent calculation object associated with self can be assigned # using the :parent hash key passed as an argument. # # Unless otherwise configured within the passed block, several attributes # attempt to take default configurations if possible using rules of thumb: # # * value => nil # * enabled => true # * visible => true # * label => underscored, symbolized version of path # * path => stringified version of label # * name => stringified and humanised version of label # * unit => default_unit # * per_unit => default_per_unit # def initialize(options={},&block) @parent=options[:parent] @value=nil @type=nil @enabled=true @visible=true instance_eval(&block) if block label path.to_s.underscore.to_sym unless path.blank?||label path label.to_s unless path name label.to_s.humanize unless name unit default_unit unless unit per_unit default_per_unit unless per_unit end # Valid choices for suggested interfaces for a term. # Dynamic boolean methods (such as text_box?) are generated for # checking which value is set. # # my_term.drop_down? #=> true # Interfaces=[:text_box,:drop_down,:date] Interfaces.each do |inf| define_method("#{inf.to_s}?") { interface==inf } end # Symbolized attribute representing the expected interface type for # self. Set a value by passing an argument. Retrieve a value by # calling without an argument, e.g., # # my_term.interface :drop_down # # my_term.interface #=> :drop_down # # Must represent one of the valid choices defined in the # Term::Interfaces constant # # If the provided interface is not valid (as defined in Term::Interfaces) # an InvalidInterface exception is raised # def interface(inf=nil) if inf raise Exceptions::InvalidInterface unless Interfaces.include? inf @interface=inf end return @interface end # Object representing the value which self is considered to # represent (e.g. the quantity or name of something). Set a value by # passing an argument. Retrieve a value by calling without an argument, # e.g., # # my_term.value 12 # my_term.value #=> 12 # # # my_term.value 'Ford Escort' # my_term.value #=> 'Ford Escort' # # # my_term.value DateTime.civil(2010,12,31) # my_term.value #=> # def value(*args) unless args.empty? @value_before_cast = args.first @value = @type ? self.class.convert_value_to_type(args.first, @type) : args.first end @value end # String representing an annotation for self. Set a value by # passing an argument. Retrieve a value by calling without an argument, # e.g., # # my_term.note 'Enter the mass of cement produced in the reporting period' # # my_term.note #=> 'Enter the mass of cement ...' # def note(string=nil) instance_variable_set("@note",string.gsub('"',"'")) unless string.nil? instance_variable_get("@note") end # Symbols representing the attributes of self which are concerned # with quantity units. # # Each symbol also represents dynamically defined method name for # setting and retrieving the default and current units and per units. Units # are initialized as instances of Quantify::Unit::Base is required. # # Set a unit attribute by passing an argument. Retrieve a value by calling # without an argument. Unit attributes can be defined by any form which is # accepted by the Quantify::Unit#for method (either an instance of # Quantify::Unit::Base (or subclass) or a symbolized or string # representation of the a unit symbol, name or label). E.g., # # my_term.unit :mi # my_term.unit #=> # # my_term.default_unit 'feet' # my_term.default_unit #=> # # # my_time_unit = Unit.hour #=> # my_term.default_per_unit my_time_unit # my_term.default_per_unit #=> # # # Dynamically defined methods are also available for setting and retrieving # alternative units for the unit and per_unit attributes. # If no alternative units are explicitly defined, they are instantiated by # default to represent all dimensionally equivalent units available in the # system of units defined by Quantify. E.g. # # my_term.unit :kg # my_term.alternative_units #=> [ , # , # , # ... ] # # my_term.unit 'litre' # my_term.alternative_units :bbl, :gal # my_term.alternative_units #=> [ , # ] # UnitFields = [:unit,:per_unit,:default_unit,:default_per_unit] UnitFields.each do |field| define_method(field) do |*unit| instance_variable_set("@#{field}",Unit.for(unit.first)) unless unit.empty? instance_variable_get("@#{field}") end end [:unit,:per_unit].each do |field| # If no argument provided, returns the alternative units which are valid # for self. If a list of units are provided as an argument, these # override the dynamically assigned alternative units for self. # define_method("alternative_#{field}s") do |*args| ivar = "@alternative_#{field}s" unless args.empty? units = args.map {|arg| Unit.for(arg) } Term.validate_dimensional_equivalence?(*units) instance_variable_set(ivar, units) else return instance_variable_get(ivar) if instance_variable_get(ivar) default = send("default_#{field}".to_sym) return instance_variable_set(ivar, (default.alternatives)) if default end end # Returns the list of unit choices for self, including both the # default unit and all alternative units. # define_method("#{field}_choices") do |*args| choices = send("alternative_#{field}s".to_sym) default = send("default_#{field}".to_sym) choices = [default] + choices if default return choices end end # Returns true if self has a populated value attribute. # Otherwise, returns false. # def set? !value_before_cast.nil? end # Returns true if self does not have a populated value # attribute. Otherwise, returns false. # def unset? value_before_cast.nil? end # Declare that the term's UI element should be disabled def disable! @disabled=true end # Declare that the term's UI element should be enabled def enable! @disabled=false end # Returns true if the UI element of self is disabled. # Otherwise, returns false. # def disabled? @disabled end # Returns true if the UI element of self is enabled. # Otherwise, returns false. # def enabled? !disabled? end # Returns true if self is configured as visible. # Otherwise, returns false. # def visible? @visible end # Returns true if self is configured as hidden. # Otherwise, returns false. # def hidden? !visible? end # Declare that the term's UI element should not be shown in generated UIs. def hide! @visible=false end # Declare that the term's UI element should be shown in generated UIs. def show! @visible=true end def ==(other_term) !TermsList::TermProperties.inject(false) do |boolean,prop| boolean || self.send(prop) != other_term.send(prop) end end # Returns true if self has a numeric value. That is, can # it have statistics applied? This method permits handling of term summing, # averaging, etc. Otherwise, returns false. # def has_numeric_value? is_numeric? && set? && Float(value) rescue false end def is_numeric? ![:string, :text, :datetime, :time, :date ].include?(type) end # Returns a pretty print string representation of self def inspect elements = {:label => label, :value => value, :unit => unit, :per_unit => per_unit, :type => type, :disabled => disabled?, :visible => visible?} attr_list = elements.map {|k,v| "#{k}: #{v.inspect}" } * ', ' "<#{self.class.name} #{attr_list}>" end # Returns true if self occurs before the term with a label # matching lab in the terms list of the parent calculation. Otherwise, # returns false. # def before?(lab) parent.terms.labels.index(lab)>parent.terms.labels.index(label) end # Returns true if self occurs after the term with a label # matching lab in the terms list of the parent calculation. Otherwise, # returns false. # def after?(lab) parent.terms.labels.index(lab)Term, based on self but with # a change of units, according to the options hash provided, and # the value attribute updated to reflect the new units. # # To specify a new unit, pass the required unit via the :unit key. # To specify a new per_unit, pass the required per unit via the # :per_unit key. E.g., # # my_term.convert_unit(:unit => :kg) # # my_term.convert_unit(:unit => :kg, :per_unit => :h) # # my_term.convert_unit(:unit => 'kilogram') # # my_term.convert_unit(:per_unit => Quantify::Unit.h) # # my_term.convert_unit(:unit => ) # # If self does not hold a numeric value or either a unit or per # unit attribute, self is returned. # def convert_unit(options={}) return self unless is_numeric? && (unit || per_unit) new = clone if has_numeric_value? if options[:unit] && unit new_unit = Unit.for(options[:unit]) Term.validate_dimensional_equivalence?(unit,new_unit) new.value Quantity.new(new.value,new.unit).to(new_unit).value end if options[:per_unit] && per_unit new_per_unit = Unit.for(options[:per_unit]) Term.validate_dimensional_equivalence?(per_unit,new_per_unit) new.value Quantity.new(new.value,(1/new.per_unit)).to(Unit.for(new_per_unit)).value end end new.unit options[:unit] if options[:unit] new.per_unit options[:per_unit] if options[:per_unit] return new end # Return an instance of Quantify::Quantity describing the quantity represented # by self. # # If self does not contain a numeric value, nil is returned. # # If self contains a numeric value, but no unit or per unit, just # the numeric value is returned # def to_quantity return nil unless has_numeric_value? if (unit.is_a? Quantify::Unit::Base) && (per_unit.is_a? Quantify::Unit::Base) quantity_unit = unit/per_unit elsif unit.is_a? Quantify::Unit::Base quantity_unit = unit elsif per_unit.is_a? Quantify::Unit::Base quantity_unit = 1/per_unit else return value end Quantity.new(value,quantity_unit) end alias :to_q :to_quantity # Returns a string representation of term based on the term value and any # units which are defined. The format of the unit representation follows # that defined by format, which should represent any of the formats # supported by the Quantify::Unit::Base class (i.e. :name, # :pluralized_name, :symbol and :label). Default behaviour uses the unit # symbol atribute, i.e. if no format explcitly specified: # # my_term.to_s #=> "12345 ton" # # my_term.to_s :symbol #=> "12345 ton" # # my_term.to_s :name #=> "12345 short ton" # # my_term.to_s :pluralized_name #=> "12345 tons" # # my_term.to_s :label #=> "12345 ton_us" # def to_s(format=:symbol) if has_numeric_value? && (unit || per_unit) self.to_quantity.to_s(format) else "#{value}" end end # Checks that the units included in units are dimensionally # equivalent, that is, that they represent the same physucal quantity # def self.validate_dimensional_equivalence?(*units) unless [units].flatten.all? {|unit| unit.dimensions == units[0].dimensions } raise AMEE::DataAbstraction::Exceptions::InvalidUnits, "The specified term units are not of equivalent dimensions: #{units.map(&:label).join(",")}" end end def self.convert_value_to_type(value, type) return nil if value.blank? type = type.downcase.to_sym if type.is_a?(String) case type when :string then value.to_s when :text then value.to_s when :integer then value.to_i rescue 0 when :fixnum then value.to_i rescue 0 when :float then value.to_f rescue 0 when :decimal then value.to_s.to_d rescue 0 when :double then value.to_s.to_d rescue 0 when :datetime then DateTime.parse(value.to_s) rescue nil when :time then Time.parse(value.to_s) rescue nil when :date then Date.parse(value.to_s) rescue nil else value end end end end end