newmen/versatile-diamond

View on GitHub
analyzer/lib/tools/dimension.rb

Summary

Maintainability
C
1 day
Test Coverage
module VersatileDiamond
  module Tools

    # Converts the passing values in concrete dimension to self dimension
    # values
    class Dimension

      # Universal gas constant
      # Could not be changed!!
      R = 8.3144621
      R_DIMENSION = 'J/(mol * K)'.freeze

      # These values are using into calculation program on C++ by default
      # On there values bases convert_value methods in this class
      # Could not be changed!!
      DEFAULT_TEMPERATURE = 'K'.freeze
      DEFAULT_CONCENTRATION = 'mol/cm3'.freeze
      DEFAULT_ENERGY = 'J/mol'.freeze
      DEFAULT_RATE = '1/s'.freeze
      DEFAULT_TIME = 's'.freeze

      # Varibales which could be converted
      SIMPLE_ZEROFILL_VARIABLES = %w(concentration energy time).freeze
      ANOTHER_VARIABLES = %w(rate temperature).freeze
      ALL_VARIABLES = (SIMPLE_ZEROFILL_VARIABLES + ANOTHER_VARIABLES).freeze

      class << self
        include Modules::SyntaxChecker

        # For RSpec
        def reset!
          ALL_VARIABLES.each do |var|
            instance_variable_set("@#{var}".to_sym, nil)
          end
        end

        ALL_VARIABLES.each do |var|
          # Defines setting method adjusts dimension of which to produce
          # convertion by default
          #
          # @param [String] dimension
          define_method("#{var}_dimension") do |dimension|
            # TODO: dimension value need to check by regex
            instance_variable_set("@#{var}".to_sym, dimension)
          end
        end

        SIMPLE_ZEROFILL_VARIABLES.each do |var|
          # Defines public convert method for all vars except rate. Each
          # method converts variable value from passed dimension to
          # self value.
          #
          # @param [Float] value which will be recalculated
          # @param [String] dimension from which will be convertation, if not
          #   passed then defaul dimension will be used
          # @raise [Errors::SyntaxError] if cannot be converted from
          #   passed dimension, or dimension is not passed and default
          #   is not setted
          # @return [Float] converted value
          define_method(:"convert_#{var}") do |value, dimension = nil|
            value == 0 ? 0 : convert(var.to_sym, value, dimension)
          end
        end

        # Converts the temperature value to default temperature dimenstion
        # @param [Float] value see as #convert_{var}
        # @param [String] dimension see as #convert_{var}
        # @return [Float] converted temperature value
        def convert_temperature(value, dimension = nil)
          convert(:temperature, value, dimension)
        end

        # Converts the rate and checks dimension in accordance with number of
        # gas phase species
        #
        # @param [Float] value see as #convert_{var}
        # @param [Integer] gases_num the number of gases involved in reaction
        # @param [String] dimension see as #convert_{var}
        # @raise [Errors::SyntaxError] see as #convert_{var}
        # @return [Float] converted rate value
        def convert_rate(value, gases_num, dimension = nil)
          return 0 if value == 0

          dimension ||= @rate
          syntax_error('.is_not_set') unless dimension

          if gases_num > 0 || dimension != DEFAULT_RATE
            _, dividend, _, divisor =
              dimension.gsub(/\(|\)/, '|').gsub(/\s/, '').
                scan(%r{\A(\|)?([^\/]+)\1?/(\|)?(.+?)\3?\Z}).first

            syntax_error('.undefined_value') unless dividend && divisor

            dividends = dividend.split('*').flat_map do |v|
              m = /\A(?<units>.*?(?<symbol>m|l))(?<degree>\d+)?\Z/.match(v)
              if m
                units = m[:units]
                symbol = m[:symbol]
                degree = m[:degree] && m[:degree].to_i

                if symbol == 'm' && degree % 3 == 0
                  next ["#{units}3"] * (degree / 3)
                elsif symbol == 'l' && units == symbol && degree
                  next ['l'] * degree
                end
              end
              v
            end
            divisors = divisor.split('*').flat_map do |v|
              m = /\A(?<units>.*?)(?<degree>\d+)?\Z/.match(v)
              if m
                units, degree = m[:units], m[:degree] && m[:degree].to_i
                next [units] * degree if degree
              end
              v
            end

            aos = { 'mol' => 1, 'kmol' => 1e3 }
            vol = {
              'mm3' => 1e3,
              'cm3' => 1,
              'dm3' => 1e-3,
              'l' => 1e-3,
              'm3' => 1e-6
            }

            coef = 1
            reduct = -> part, d, c do
              (i = part.index(d)) && part.delete_at(i) && (coef *= c)
            end

            gases_num.times do
              aos.any? { |d, c| reduct[divisors, d, c] } ||
                syntax_error('.undefined_value')

              vol.any? { |d, c| reduct[dividends, d, c] } ||
                syntax_error('.undefined_value')
            end

            dimension = ''
            dimension << (dividends.empty? ? '1' : dividends.join('*'))
            unless divisors.empty?
              divisor = divisors.join('*')
              divisor = "(#{divisor})" if divisors.size > 1
              dimension << "/#{divisor}"
            end
            dimension = '' if dimension == '1'

            if dimension != DEFAULT_RATE.gsub(/\s/, '')
              syntax_error('.undefined_value')
            end

            value * coef
          else
            value
          end
        end

      private

        # Converts variable value to setuped dimension
        # @param [Symbol] var the convertable variable
        # @param [Float] value the convertable value
        # @param [String] convertable_dimension the dimension to that will be
        #   converting
        # @raise [Errors::SyntaxError] if value cannot be converted
        # @return [Float] converted value
        def convert(var, value, convertable_dimension = nil)
          convertable_dimension && convertable_dimension.strip!

          current_dimension = instance_variable_get("@#{var}".to_sym)
          unless current_dimension || convertable_dimension
            syntax_error('.is_not_set')
          end

          default_dimension = eval("DEFAULT_#{var.to_s.upcase}")
          if (!convertable_dimension && current_dimension == default_dimension) ||
            convertable_dimension == default_dimension

            return value
          end

          send("convert_#{var}_laws", value,
            convertable_dimension || current_dimension)
        end

        class << self
        private
          # Defines internal static convert method
          # @param [Symbol] var converting variable name
          # @param [Hash] cases is the hash with matching regex as keys and
          #   converting lambdas as values
          def define_convert(var, cases)
            define_method("convert_#{var}_laws") do |value, dimension|
              convert_value(value, dimension, cases) # described at the end
            end
          end
        end

        # Finds dimension converting lambda from passed cases
        # @param [Float] value the convertable value
        # @param [String] dimension the dimension from that will be converting
        # @param [Hash] cases the hash that contain regex as keys and lamdas as
        #   values
        # @raise [Errors::SyntaxError] if value cannot be converted
        # @return [Float] converted value
        def convert_value(value, dimension, cases)
          _, func = cases.find { |matcher, _| matcher.match(dimension) }
          func ? func[value] : syntax_error('.undefined_value')
        end

        define_convert(:temperature, {
          'K' => -> v { v },
          'C' => -> v { v + 273.15 },
          'F' => -> v { (v + 459.67) / 1.8 }
        })

        define_convert(:concentration, {
          /\Amol\s*\/\s*mm3\Z/ => -> v { v * 1e3 },
          /\Amol\s*\/\s*cm3\Z/ => -> v { v },
          /\Amol\s*\/\s*(:?dm3|l)\Z/ => -> v { v * 1e-3 },
          /\Amol\s*\/\s*m3\Z/ => -> v { v * 1e-6 },
          /\Akmol\s*\/\s*mm3\Z/ => -> v { v * 1e6 },
          /\Akmol\s*\/\s*cm3\Z/ => -> v { v * 1e3 },
          /\Akmol\s*\/\s*(:?dm3|l)\Z/ => -> v { v },
          /\Akmol\s*\/\s*m3\Z/ => -> v { v * 1e-3 }
        })

        define_convert(:energy, {
          /\AJ\s*\/\s*mol\Z/ => -> v { v },
          /\AkJ\s*\/\s*mol\Z/ => -> v { v * 1e3 },
          /\AkJ\s*\/\s*kmol\Z/ => -> v { v },
          /\Akcal\s*\/\s*mol\Z/ => -> v { v * 4184 },
          /\Akcal\s*\/\s*kmol\Z/ => -> v { v * 4.184 },
          /\Acal\s*\/\s*mol\Z/ => -> v { v * 4.184 }
        })

        define_convert(:time, {
          /\As(?:ec)?\Z/ => -> v { v },
          /\Am(?:in)?s?\Z/ => -> v { v * 60 },
          /\Ah(?:our)?s?\Z/ => -> v { v * 3600 }
        })
      end
    end

  end
end