Class: Plurimath::Math::Evaluation::Evaluator

Inherits:
Object
  • Object
show all
Defined in:
lib/plurimath/math/evaluation/evaluator.rb

Overview

Computes the numeric value of a Formula tree against variable bindings, enforcing the strict error contract: results are always real, finite numbers or one of the Errors::Evaluation classes is raised.

Instance Method Summary collapse

Constructor Details

#initialize(formula, bindings = {}) ⇒ Evaluator

Returns a new instance of Evaluator.



11
12
13
14
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 11

def initialize(formula, bindings = {})
  @formula = formula
  @bindings = normalize_bindings(bindings)
end

Instance Method Details

#divide(dividend, divisor) ⇒ Object



57
58
59
60
61
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 57

def divide(dividend, divisor)
  raise Errors::Evaluation::DivisionByZeroError if divisor.zero?

  dividend / divisor.to_f
end

#evaluateObject



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 16

def evaluate
  result = begin
    evaluate_formula(@formula)
  rescue ::Math::DomainError => e
    raise Errors::Evaluation::MathDomainError, e.message
  rescue ::FloatDomainError
    raise Errors::Evaluation::NonFiniteResultError
  rescue ::ZeroDivisionError
    raise Errors::Evaluation::DivisionByZeroError
  end

  raise Errors::Evaluation::NonFiniteResultError unless result.finite?

  result
end

#evaluate_arguments(nodes) ⇒ Object

Comma-separated argument lists for functions like ‘max(2,3)`.



84
85
86
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 84

def evaluate_arguments(nodes)
  split_on_commas(Array(nodes)).map { |segment| evaluate_nodes(segment) }
end

#evaluate_formula(formula) ⇒ Object



32
33
34
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 32

def evaluate_formula(formula)
  real_result(ExpressionParser.new(self, formula.value).parse)
end

#evaluate_node(node) ⇒ Object



40
41
42
43
44
45
46
47
48
49
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 40

def evaluate_node(node)
  case node
  when nil
    unsupported("missing operand")
  when Formula
    evaluate_formula(node)
  else
    real_result(node.evaluate(self))
  end
end

#evaluate_nodes(nodes) ⇒ Object



36
37
38
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 36

def evaluate_nodes(nodes)
  evaluate_formula(Formula.new(Array(nodes)))
end

#function_arguments(node) ⇒ Object



88
89
90
91
92
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 88

def function_arguments(node)
  return evaluate_arguments(node.parameter_two) if node.is_a?(Function::Fenced)

  evaluate_arguments(Array(node))
end

#modulo(dividend, divisor) ⇒ Object



63
64
65
66
67
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 63

def modulo(dividend, divisor)
  raise Errors::Evaluation::DivisionByZeroError if divisor.zero?

  dividend % divisor
end

#power(base, exponent) ⇒ Object



69
70
71
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 69

def power(base, exponent)
  real_result(base**exponent)
end

#real_result(value) ⇒ Object

Non-real values are rejected per subexpression so they cannot reach other numeric operations. Non-finite values are only rejected on the final result, so correct asymptotic values like ‘1/exp(1000)` still evaluate.



77
78
79
80
81
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 77

def real_result(value)
  raise Errors::Evaluation::MathDomainError, "result is not a real number" unless value.real?

  value
end

#unsupported(node_or_message) ⇒ Object



105
106
107
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 105

def unsupported(node_or_message)
  raise Errors::Evaluation::UnsupportedExpressionError, unsupported_message(node_or_message)
end

#value_for(name) ⇒ Object



51
52
53
54
55
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 51

def value_for(name)
  raise Errors::Evaluation::MissingVariableError, name unless bindings.key?(name)

  bindings[name]
end

#with_binding(name, value) ⇒ Object

Temporarily binds an iteration index, shadowing any outer binding of the same name and restoring it afterwards.



96
97
98
99
100
101
102
103
# File 'lib/plurimath/math/evaluation/evaluator.rb', line 96

def with_binding(name, value)
  had_key = bindings.key?(name)
  previous = bindings[name]
  bindings[name] = value
  yield
ensure
  had_key ? bindings[name] = previous : bindings.delete(name)
end