newmen/versatile-diamond

View on GitHub
analyzer/lib/concepts/specific_atom.rb

Summary

Maintainability
A
0 mins
Test Coverage
module VersatileDiamond
  using Patches::RichArray

  module Concepts

    # Specified atom class, contain additional atom states like incoherentness,
    # unfixness and activeness
    class SpecificAtom
      include Modules::ListsComparer
      extend Forwardable

      # Error for case when something wrong with atom state
      # @abstract
      class Stated < Errors::Base
        attr_reader :state
        def initialize(state); @state = state end
      end

      # Error for case if state for atom already exsit
      class AlreadyStated < Stated; end

      # Error for case if state for atom doesn't exsit
      class NotStated < Stated; end

      # Error for case if unfixed state is stated but incoherent state states now
      class AlreadyUnfixed < Errors::Base; end

      def_delegators :atom, :name, :lattice, :lattice=, :original_valence,
        :original_same?, :reference?, :relations_limits

      attr_reader :monovalents

      # Initialize a new instance
      # @param [Atom] atom the specified atom
      # @option [SpecificAtom] :ancestor the ancestor of new atom
      # @option [Array] :options the atom configuration (not using if ancestor
      #   passed)
      # @option [Array] :monovalents names of monovalent atoms with which
      #   the current atom is bonded
      def initialize(atom, ancestor: nil, options: [], monovalents: [])
        @atom = atom.dup # because atom can be changed by mapping algorithm
        @options = ancestor ? ancestor.options.dup : options
        @monovalents = ancestor ? ancestor.monovalents.dup : monovalents
      end

      # Makes copy of another instance
      # @param [SpecificAtom] other an other specified atom
      def initialize_copy(other)
        @atom = other.atom.dup
        @options = other.options.dup
        @monovalents = other.monovalents.dup
      end

      # Gets valence of specific atom
      # @return [Integer] the number of valence bonds
      def valence
        atom.valence - actives - monovalents.size
      end

      # Gets the unspecified atom instance
      # @return [Atom | AtomReference] without specific states
      def clean
        atom
      end

      # Specific atom could be not specified
      # @return [Boolean] is specified or not
      def specific?
        !(options.empty? && monovalents.empty?)
      end

      # Compares current instance with other
      # @param [Atom | AtomReference | SpecificAtom] other the other atom with
      #   which comparing do
      # @return [Boolean] is the same atom or not
      def same?(other)
        if self.class == other.class
          atom.same?(other.atom) && equal_properties?(other)
        elsif other.is_a?(VeiledAtom)
          other.same?(self)
        else
          options.empty? && monovalents.empty? && atom.same?(other)
        end
      end

      # @param [Atom | AtomReference | SpecificAtom] other comparing atom
      # @return [Boolean] are accurate same atoms or not
      def accurate_same?(other)
        (self.class == other.class &&
                  atom.accurate_same?(other.atom) && equal_properties?(other)) ||
          (other.is_a?(VeiledAtom) && accurate_same?(other.original))
      end

      # Setup monovalent atom for using it
      # @param [Atom] atom the monovalent atom which is used as one of bond
      def use!(atom)
        @monovalents << AtomicSpec.new(atom)
      end

      # Activates atom instance
      def active!
        @options << ActiveBond.property
      end

      # Changes atom incoherent state
      # @raise [AlreadyStated] if atom already has incoherent state
      def incoherent!
        raise AlreadyStated.new('incoherent') if incoherent?
        is_unfixed = unfixed?
        not_unfixed! if is_unfixed
        @options << Incoherent.property
        raise AlreadyUnfixed.new if is_unfixed
      end

      # Changes atom unfixed state
      # @raise [AlreadyStated] if atom already has unfixed state
      def unfixed!
        raise AlreadyStated.new('unfixed') if incoherent? || unfixed?
        @options << Unfixed.property
      end

      [Incoherent, Unfixed].each do |klass|
        state = klass.to_s.split('::').last.downcase
        relevant_property = klass.property
        # Defines methods for checking atom state
        # @return [Boolean] is atom has state or not
        define_method(:"#{state}?") do
          options.include?(relevant_property)
        end

        # Defines methods for unsetup atom state
        # @raise [NotStated] if atom doesn't have target state
        define_method(:"not_#{state}!") do
          raise NotStated.new(state) unless send(:"#{state}?")
          options.delete(relevant_property)
        end
      end

      # Counts active bonds
      # @return [Integer] the number of active bonds
      def actives
        active_options.size
      end

      # Compares relevant states with states of another atom. Used in Hanser's
      # algorithm.
      #
      # @param [Atom | AtomReference | SpecificAtom] other the atom with which
      #   compares
      # @return [Array] the array of relevants state symbols
      def diff(other)
        actives_delta = actives - other.actives
        actives_delta = 0 if actives_delta < 0
        (other.relevants - relevants) +
          monovalents.accurate_diff(other.monovalents) +
          ([ActiveBond.property] * actives_delta)
      end

      # Applies diff to current options
      # @param [Array] diff the array which contain adsorbing states
      def apply_diff(diff)
        diff.each { |property| property.apply_to(self) }
      end

      # Gets only relevant states
      # @return [Array] the array of relevant states
      def relevants
        opts_without_actives = options.dup
        opts_without_actives.delete(ActiveBond.property)
        (opts_without_actives + atom.relevants).uniq
      end

      # Provides additional valence states of current atom
      # @return [Array] the array of relations
      def additional_relations
        own_links = (options + monovalents).map { |state| [self, state] }
        atom.additional_relations + own_links
      end

      def to_s
        chars = (options + monovalents).map(&:to_s)
        "#{atom}[#{chars.sort.join(', ')}]"
      end

      def inspect
        to_s
      end

    protected

      attr_reader :atom, :options

    private

      # Selects from options only active bonds
      # @return [Array] array of active bonds
      def active_options
        active_bond = ActiveBond.property
        options.select { |o| o == active_bond }
      end

      # @return [Boolean] are equal properties of self and other atoms or not
      def equal_properties?(other)
        lists_are_identical?(options, other.options) &&
          lists_are_identical?(monovalents, other.monovalents)
      end
    end

  end
end