class Dentaku::BulkExpressionSolver
def self.dependency_cache
def self.dependency_cache @dep_cache ||= {} end
def add_dependencies(current_dependencies, variable)
def add_dependencies(current_dependencies, variable) node = calculator.memory[variable] if node.respond_to?(:dependencies) current_dependencies[variable] = node.dependencies node.dependencies.each { |d| add_dependencies(current_dependencies, d) } end end
def dependencies
def dependencies Hash[expression_deps].tap do |d| d.values.each do |deps| unresolved = deps.reject { |ud| d.has_key?(ud) } unresolved.each { |u| add_dependencies(d, u) } end end end
def expression_deps
def expression_deps expressions.map do |var, expr| [var, calculator.dependencies(expr)] end end
def expression_with_exception_handler(var_name, &block)
def expression_with_exception_handler(var_name, &block) ->(_expr, ex) { ex.recipient_variable = var_name block.call(ex) } end
def expressions
def expressions @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }] end
def initialize(expressions, calculator)
def initialize(expressions, calculator) @expression_hash = FlatHash.from_hash(expressions) @calculator = calculator end
def load_results(&block)
def load_results(&block) facts, _formulas = expressions.transform_keys(&:downcase) .transform_values { |v| calculator.ast(v) } .partition { |_, v| calculator.dependencies(v, nil).empty? } evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h| with_rescues(var_name, h, block) do h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value end end context = calculator.memory.merge(evaluated_facts) variables_in_resolve_order.each_with_object({}) do |var_name, results| next if expressions[var_name].nil? with_rescues(var_name, results, block) do results[var_name] = evaluated_facts[var_name] || evaluator.evaluate( expressions[var_name], context.merge(results), &expression_with_exception_handler(var_name, &block) ).tap { |res| res.recipient_variable = var_name if res.respond_to?(:recipient_variable=) res } end end rescue TSort::Cyclic => ex block.call(ex) {} end
def raise_exception_handler
def raise_exception_handler ->(ex) { raise ex } end
def return_undefined_handler
def return_undefined_handler ->(*) { :undefined } end
def solve(&block)
def solve(&block) @evaluator ||= PermissiveEvaluator.new(calculator, block) error_handler = block || return_undefined_handler results = load_results(&error_handler) FlatHash.expand( expression_hash.each_with_object({}) do |(k, v), r| default = v.nil? ? v : :undefined r[k] = results.fetch(k.to_s, default) end ) end
def solve!
def solve! @evaluator = StrictEvaluator.new(calculator) solve(&raise_exception_handler) end
def variables_in_resolve_order
def variables_in_resolve_order cache_key = expressions.keys.map(&:to_s).sort.join("|") @ordered_deps ||= self.class.dependency_cache.fetch(cache_key) { DependencyResolver.find_resolve_order(dependencies).tap do |d| self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order? end } end
def with_rescues(var_name, results, block)
def with_rescues(var_name, results, block) yield rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex ex.recipient_variable = var_name results[var_name] = block.call(ex) ensure if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase) results[var_name] = calculator.memory[var_name.downcase] end end