plurimath 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +108 -175
- data/Gemfile +1 -0
- data/README.adoc +282 -6
- data/lib/plurimath/asciimath/parse.rb +6 -1
- data/lib/plurimath/asciimath/transform.rb +2 -0
- data/lib/plurimath/base_number_prefix.rb +43 -0
- data/lib/plurimath/configuration.rb +9 -1
- data/lib/plurimath/errors/evaluation/division_by_zero_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/error.rb +9 -0
- data/lib/plurimath/errors/evaluation/invalid_binding_error.rb +14 -0
- data/lib/plurimath/errors/evaluation/invalid_binding_key_error.rb +14 -0
- data/lib/plurimath/errors/evaluation/math_domain_error.rb +9 -0
- data/lib/plurimath/errors/evaluation/missing_variable_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/non_finite_result_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/unsupported_expression_error.rb +13 -0
- data/lib/plurimath/errors/evaluation.rb +18 -0
- data/lib/plurimath/errors.rb +1 -0
- data/lib/plurimath/formatter/numbers/base_notation.rb +54 -31
- data/lib/plurimath/formatter/numbers/formatted_notation.rb +62 -0
- data/lib/plurimath/formatter/numbers/formatted_number.rb +87 -0
- data/lib/plurimath/formatter/numbers/fraction.rb +1 -1
- data/lib/plurimath/formatter/numbers/mathml_renderer.rb +56 -0
- data/lib/plurimath/formatter/numbers/notation_renderer.rb +30 -29
- data/lib/plurimath/formatter/numbers/number_renderer.rb +10 -9
- data/lib/plurimath/formatter/numbers/omml_renderer.rb +74 -0
- data/lib/plurimath/formatter/numbers/source.rb +29 -4
- data/lib/plurimath/formatter/numbers/text_renderer.rb +52 -0
- data/lib/plurimath/formatter/numbers.rb +6 -2
- data/lib/plurimath/html/parse.rb +5 -0
- data/lib/plurimath/html/transform.rb +2 -0
- data/lib/plurimath/latex/parse.rb +5 -0
- data/lib/plurimath/latex/transform.rb +2 -0
- data/lib/plurimath/math/core.rb +52 -0
- data/lib/plurimath/math/evaluation/evaluator.rb +147 -0
- data/lib/plurimath/math/evaluation/expression_parser.rb +215 -0
- data/lib/plurimath/math/evaluation/iteration.rb +63 -0
- data/lib/plurimath/math/evaluation.rb +13 -0
- data/lib/plurimath/math/formula.rb +9 -0
- data/lib/plurimath/math/function/abs.rb +4 -0
- data/lib/plurimath/math/function/arccos.rb +4 -0
- data/lib/plurimath/math/function/arcsin.rb +4 -0
- data/lib/plurimath/math/function/arctan.rb +4 -0
- data/lib/plurimath/math/function/ceil.rb +4 -0
- data/lib/plurimath/math/function/cos.rb +4 -0
- data/lib/plurimath/math/function/cosh.rb +4 -0
- data/lib/plurimath/math/function/cot.rb +4 -0
- data/lib/plurimath/math/function/coth.rb +4 -0
- data/lib/plurimath/math/function/csc.rb +4 -0
- data/lib/plurimath/math/function/csch.rb +4 -0
- data/lib/plurimath/math/function/exp.rb +4 -0
- data/lib/plurimath/math/function/fenced.rb +4 -0
- data/lib/plurimath/math/function/floor.rb +4 -0
- data/lib/plurimath/math/function/frac.rb +7 -0
- data/lib/plurimath/math/function/gcd.rb +9 -0
- data/lib/plurimath/math/function/lcm.rb +9 -0
- data/lib/plurimath/math/function/lg.rb +4 -0
- data/lib/plurimath/math/function/ln.rb +4 -0
- data/lib/plurimath/math/function/log.rb +19 -0
- data/lib/plurimath/math/function/max.rb +4 -0
- data/lib/plurimath/math/function/min.rb +4 -0
- data/lib/plurimath/math/function/mod.rb +15 -0
- data/lib/plurimath/math/function/power.rb +10 -0
- data/lib/plurimath/math/function/prod.rb +10 -0
- data/lib/plurimath/math/function/root.rb +7 -0
- data/lib/plurimath/math/function/sec.rb +4 -0
- data/lib/plurimath/math/function/sech.rb +4 -0
- data/lib/plurimath/math/function/sin.rb +4 -0
- data/lib/plurimath/math/function/sinh.rb +4 -0
- data/lib/plurimath/math/function/sqrt.rb +4 -0
- data/lib/plurimath/math/function/sum.rb +10 -0
- data/lib/plurimath/math/function/tan.rb +4 -0
- data/lib/plurimath/math/function/tanh.rb +4 -0
- data/lib/plurimath/math/function/text.rb +17 -0
- data/lib/plurimath/math/number.rb +40 -29
- data/lib/plurimath/math/symbols/cdot.rb +4 -0
- data/lib/plurimath/math/symbols/div.rb +4 -0
- data/lib/plurimath/math/symbols/hat.rb +4 -0
- data/lib/plurimath/math/symbols/minus.rb +4 -0
- data/lib/plurimath/math/symbols/pi.rb +5 -1
- data/lib/plurimath/math/symbols/plus.rb +4 -0
- data/lib/plurimath/math/symbols/slash.rb +4 -0
- data/lib/plurimath/math/symbols/symbol.rb +45 -0
- data/lib/plurimath/math/symbols/times.rb +4 -0
- data/lib/plurimath/math.rb +1 -0
- data/lib/plurimath/mathml/constants.rb +18 -0
- data/lib/plurimath/number_formatter.rb +47 -28
- data/lib/plurimath/setup/opal.rb.erb +13 -0
- data/lib/plurimath/unicode_math/parse.rb +5 -1
- data/lib/plurimath/unicode_math/transform.rb +469 -755
- data/lib/plurimath/utility.rb +1 -1
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +1 -0
- metadata +21 -3
- data/lib/plurimath/formatter/numbers/parts_renderer.rb +0 -30
data/lib/plurimath/math/core.rb
CHANGED
|
@@ -353,6 +353,58 @@ namespace: "m")
|
|
|
353
353
|
false
|
|
354
354
|
end
|
|
355
355
|
|
|
356
|
+
def plus_operator?
|
|
357
|
+
false
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def minus_operator?
|
|
361
|
+
false
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def multiply_operator?
|
|
365
|
+
false
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def divide_operator?
|
|
369
|
+
false
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def power_operator?
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def operator?
|
|
377
|
+
plus_operator? || minus_operator? || multiply_operator? ||
|
|
378
|
+
divide_operator? || power_operator?
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def reserved_constant
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Default evaluation entry point: a node that does not implement a
|
|
386
|
+
# numeric evaluation raises UnsupportedExpressionError. Concrete nodes
|
|
387
|
+
# (Number, Symbol operators, Function::*) override this.
|
|
388
|
+
def evaluate(evaluator)
|
|
389
|
+
evaluator.unsupported(self)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def open?
|
|
393
|
+
false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def close?
|
|
397
|
+
false
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# The bound variable a node represents, or nil if it is not a plain
|
|
401
|
+
# variable. Symbols and text nodes that name a variable override this;
|
|
402
|
+
# it lets iteration indexes accept both `Symbols::Symbol` (AsciiMath/
|
|
403
|
+
# MathML) and `Function::Text` (OMML) without type-sniffing.
|
|
404
|
+
def variable_name
|
|
405
|
+
nil
|
|
406
|
+
end
|
|
407
|
+
|
|
356
408
|
def unicodemath_parens(field, options:)
|
|
357
409
|
paren = field.to_unicodemath(options: options)
|
|
358
410
|
return paren if field.is_a?(Math::Function::Fenced)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Computes the numeric value of a Formula tree against variable
|
|
7
|
+
# bindings, enforcing the strict error contract: results are always
|
|
8
|
+
# real, finite numbers or one of the Errors::Evaluation classes is
|
|
9
|
+
# raised.
|
|
10
|
+
class Evaluator
|
|
11
|
+
def initialize(formula, bindings = {})
|
|
12
|
+
@formula = formula
|
|
13
|
+
@bindings = normalize_bindings(bindings)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def evaluate
|
|
17
|
+
result = begin
|
|
18
|
+
evaluate_formula(@formula)
|
|
19
|
+
rescue ::Math::DomainError => e
|
|
20
|
+
raise Errors::Evaluation::MathDomainError, e.message
|
|
21
|
+
rescue ::FloatDomainError
|
|
22
|
+
raise Errors::Evaluation::NonFiniteResultError
|
|
23
|
+
rescue ::ZeroDivisionError
|
|
24
|
+
raise Errors::Evaluation::DivisionByZeroError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise Errors::Evaluation::NonFiniteResultError unless result.finite?
|
|
28
|
+
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def evaluate_formula(formula)
|
|
33
|
+
real_result(ExpressionParser.new(self, formula.value).parse)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate_nodes(nodes)
|
|
37
|
+
evaluate_formula(Formula.new(Array(nodes)))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def evaluate_node(node)
|
|
41
|
+
case node
|
|
42
|
+
when nil
|
|
43
|
+
unsupported("missing operand")
|
|
44
|
+
when Formula
|
|
45
|
+
evaluate_formula(node)
|
|
46
|
+
else
|
|
47
|
+
real_result(node.evaluate(self))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def value_for(name)
|
|
52
|
+
raise Errors::Evaluation::MissingVariableError, name unless bindings.key?(name)
|
|
53
|
+
|
|
54
|
+
bindings[name]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def divide(dividend, divisor)
|
|
58
|
+
raise Errors::Evaluation::DivisionByZeroError if divisor.zero?
|
|
59
|
+
|
|
60
|
+
dividend / divisor.to_f
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def modulo(dividend, divisor)
|
|
64
|
+
raise Errors::Evaluation::DivisionByZeroError if divisor.zero?
|
|
65
|
+
|
|
66
|
+
dividend % divisor
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def power(base, exponent)
|
|
70
|
+
real_result(base**exponent)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Non-real values are rejected per subexpression so they cannot reach
|
|
74
|
+
# other numeric operations. Non-finite values are only rejected on the
|
|
75
|
+
# final result, so correct asymptotic values like `1/exp(1000)` still
|
|
76
|
+
# evaluate.
|
|
77
|
+
def real_result(value)
|
|
78
|
+
raise Errors::Evaluation::MathDomainError, "result is not a real number" unless value.real?
|
|
79
|
+
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Comma-separated argument lists for functions like `max(2,3)`.
|
|
84
|
+
def evaluate_arguments(nodes)
|
|
85
|
+
split_on_commas(Array(nodes)).map { |segment| evaluate_nodes(segment) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def function_arguments(node)
|
|
89
|
+
return evaluate_arguments(node.parameter_two) if node.is_a?(Function::Fenced)
|
|
90
|
+
|
|
91
|
+
evaluate_arguments(Array(node))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Temporarily binds an iteration index, shadowing any outer binding
|
|
95
|
+
# of the same name and restoring it afterwards.
|
|
96
|
+
def with_binding(name, value)
|
|
97
|
+
had_key = bindings.key?(name)
|
|
98
|
+
previous = bindings[name]
|
|
99
|
+
bindings[name] = value
|
|
100
|
+
yield
|
|
101
|
+
ensure
|
|
102
|
+
had_key ? bindings[name] = previous : bindings.delete(name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unsupported(node_or_message)
|
|
106
|
+
raise Errors::Evaluation::UnsupportedExpressionError, unsupported_message(node_or_message)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
attr_reader :bindings
|
|
112
|
+
|
|
113
|
+
def normalize_bindings(bindings)
|
|
114
|
+
bindings.to_hash.each_with_object({}) do |(key, value), normalized|
|
|
115
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
116
|
+
raise Errors::Evaluation::InvalidBindingKeyError, key
|
|
117
|
+
end
|
|
118
|
+
unless value.is_a?(Numeric) && value.real?
|
|
119
|
+
raise Errors::Evaluation::InvalidBindingError.new(key, value)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
normalized[key.to_s] = value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def split_on_commas(nodes)
|
|
127
|
+
nodes.each_with_object([[]]) do |node, segments|
|
|
128
|
+
node.is_a?(Symbols::Comma) ? segments << [] : segments.last << node
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def unsupported_message(node_or_message)
|
|
133
|
+
return node_or_message if node_or_message.is_a?(String)
|
|
134
|
+
return "equation" if node_or_message.is_a?(Symbols::Equal)
|
|
135
|
+
return "number `#{node_or_message.value}`" if node_or_message.is_a?(Number)
|
|
136
|
+
|
|
137
|
+
if node_or_message.instance_of?(Symbols::Symbol) &&
|
|
138
|
+
node_or_message.value.to_s != ""
|
|
139
|
+
return "symbol `#{node_or_message.value}`"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
node_or_message.class.name.sub(/^Plurimath::Math::/, "")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Resolves operator precedence over the flat token sequences stored in
|
|
7
|
+
# Formula#value, delegating node evaluation back to the evaluator.
|
|
8
|
+
#
|
|
9
|
+
# Standard recursive-descent precedence ladder — each level parses its
|
|
10
|
+
# operands at the next-tighter level, so the operands of `+`/`-` are
|
|
11
|
+
# whole multiplicative expressions, and so on down to single operands:
|
|
12
|
+
# parse_additive -> parse_multiplicative -> parse_unary
|
|
13
|
+
# -> parse_power -> parse_operand
|
|
14
|
+
class ExpressionParser
|
|
15
|
+
def initialize(evaluator, tokens)
|
|
16
|
+
@evaluator = evaluator
|
|
17
|
+
@tokens = tokens
|
|
18
|
+
@index = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse
|
|
22
|
+
result = parse_additive
|
|
23
|
+
evaluator.unsupported(current) unless eof?
|
|
24
|
+
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :evaluator, :tokens
|
|
31
|
+
|
|
32
|
+
def parse_additive
|
|
33
|
+
result = parse_multiplicative
|
|
34
|
+
loop do
|
|
35
|
+
if take?(:plus_operator?)
|
|
36
|
+
result += parse_multiplicative
|
|
37
|
+
elsif take?(:minus_operator?)
|
|
38
|
+
result -= parse_multiplicative
|
|
39
|
+
else
|
|
40
|
+
break result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_multiplicative
|
|
46
|
+
result = parse_unary
|
|
47
|
+
loop do
|
|
48
|
+
if take?(:multiply_operator?)
|
|
49
|
+
result *= parse_unary
|
|
50
|
+
elsif take?(:divide_operator?)
|
|
51
|
+
result = evaluator.divide(result, parse_unary)
|
|
52
|
+
elsif implicit_multiplication?
|
|
53
|
+
result *= parse_power
|
|
54
|
+
else
|
|
55
|
+
break result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_unary
|
|
61
|
+
if take?(:plus_operator?)
|
|
62
|
+
parse_unary
|
|
63
|
+
elsif take?(:minus_operator?)
|
|
64
|
+
negated_unary
|
|
65
|
+
else
|
|
66
|
+
parse_power
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Unary minus binds tighter than `mod`, so `-7 mod 3` negates the
|
|
71
|
+
# dividend, not the result: `(-7) mod 3 == 2`, not `-(7 mod 3)`.
|
|
72
|
+
def negated_unary
|
|
73
|
+
return next_token.evaluate_negated(evaluator) if current.is_a?(Function::Mod)
|
|
74
|
+
|
|
75
|
+
-parse_unary
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Chained powers evaluate left-to-right, matching the left-nested
|
|
79
|
+
# trees parsers build for chains like `2^3^2`.
|
|
80
|
+
def parse_power
|
|
81
|
+
result = parse_operand
|
|
82
|
+
result = evaluator.power(result, parse_exponent) while take?(:power_operator?)
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_exponent
|
|
87
|
+
if take?(:plus_operator?)
|
|
88
|
+
parse_exponent
|
|
89
|
+
elsif take?(:minus_operator?)
|
|
90
|
+
negated_exponent
|
|
91
|
+
else
|
|
92
|
+
parse_operand
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def negated_exponent
|
|
97
|
+
return next_token.evaluate_negated(evaluator) if current.is_a?(Function::Mod)
|
|
98
|
+
|
|
99
|
+
-parse_exponent
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_operand
|
|
103
|
+
evaluator.unsupported("empty expression") if eof?
|
|
104
|
+
return parse_group if open_paren?(current)
|
|
105
|
+
|
|
106
|
+
node = next_token
|
|
107
|
+
return bind_argument(node) if next_argument?(node)
|
|
108
|
+
return bind_log_argument(node) if log_argument?(node)
|
|
109
|
+
|
|
110
|
+
node = bind_nary_body(node) if nary_body?(node)
|
|
111
|
+
evaluator.evaluate_node(node)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A non-close token before the close paren is reported as itself;
|
|
115
|
+
# "unmatched parenthesis" is reserved for true end-of-input.
|
|
116
|
+
def parse_group
|
|
117
|
+
advance
|
|
118
|
+
result = parse_additive
|
|
119
|
+
evaluator.unsupported(current || "unmatched parenthesis") unless close_paren?(current)
|
|
120
|
+
|
|
121
|
+
advance
|
|
122
|
+
result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Unary functions and `log` parse separately from their argument in
|
|
126
|
+
# some syntaxes; both bind the following fenced group.
|
|
127
|
+
def next_argument?(node)
|
|
128
|
+
node.is_a?(Function::UnaryFunction) && node.parameter_one.nil? &&
|
|
129
|
+
current.is_a?(Function::Fenced)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def bind_argument(node)
|
|
133
|
+
evaluator.evaluate_node(node.class.new(next_token))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# MathML/OMML n-ary Sum/Prod carry their bounds but leave the body as
|
|
137
|
+
# the following sibling token; AsciiMath binds a single operand as the
|
|
138
|
+
# body, so we match by adopting the next operand as parameter_three.
|
|
139
|
+
def nary_body?(node)
|
|
140
|
+
(node.is_a?(Function::Sum) || node.is_a?(Function::Prod)) &&
|
|
141
|
+
node.parameter_three.nil? &&
|
|
142
|
+
node.parameter_one && node.parameter_two &&
|
|
143
|
+
!eof? && operand_start?(current)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The body is a single following operand; if that operand is itself a
|
|
147
|
+
# sibling-body Sum/Prod (e.g. nested MathML `sum sum …`), complete it
|
|
148
|
+
# recursively so the inner body binds too.
|
|
149
|
+
def bind_nary_body(node)
|
|
150
|
+
body = next_token
|
|
151
|
+
body = bind_nary_body(body) if nary_body?(body)
|
|
152
|
+
node.class.new(node.parameter_one, node.parameter_two, body)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def log_argument?(node)
|
|
156
|
+
node.is_a?(Function::Log) && current.is_a?(Function::Fenced)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def bind_log_argument(node)
|
|
160
|
+
evaluator.real_result(node.evaluate_with_argument(evaluator, next_token))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Adjacent operands multiply by juxtaposition (`2a`, `2(a+b)`), except
|
|
164
|
+
# two adjacent numeric literals, which usually indicate a split number
|
|
165
|
+
# literal. Signs are never consumed implicitly.
|
|
166
|
+
def implicit_multiplication?
|
|
167
|
+
operand_start?(current) &&
|
|
168
|
+
!(current.is_a?(Number) && tokens[@index - 1].is_a?(Number))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def operand_start?(node)
|
|
172
|
+
return false if node.nil? || node.operator?
|
|
173
|
+
|
|
174
|
+
!close_paren?(node)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def take?(predicate)
|
|
178
|
+
return false if eof? || !current.public_send(predicate)
|
|
179
|
+
|
|
180
|
+
advance
|
|
181
|
+
true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def next_token
|
|
185
|
+
token = current
|
|
186
|
+
advance
|
|
187
|
+
token
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def open_paren?(node)
|
|
191
|
+
node&.open?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def close_paren?(node)
|
|
195
|
+
node&.close?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def current
|
|
199
|
+
token = tokens[@index]
|
|
200
|
+
evaluator.unsupported("malformed token") unless token.nil? || token.is_a?(Core)
|
|
201
|
+
|
|
202
|
+
token
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def advance
|
|
206
|
+
@index += 1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def eof?
|
|
210
|
+
@index >= tokens.length
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Evaluates bounded Sum/Prod iterations: parses the `i=1` lower bound,
|
|
7
|
+
# validates the integer bounds and step limit, then folds the body with
|
|
8
|
+
# the index temporarily bound.
|
|
9
|
+
class Iteration
|
|
10
|
+
def initialize(evaluator, lower, upper, body)
|
|
11
|
+
@evaluator = evaluator
|
|
12
|
+
@lower = lower
|
|
13
|
+
@upper = upper
|
|
14
|
+
@body = body
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def accumulate(initial, operation)
|
|
18
|
+
name, from = index_definition
|
|
19
|
+
to = evaluator.evaluate_node(upper)
|
|
20
|
+
validate_bounds(from, to)
|
|
21
|
+
|
|
22
|
+
(from..to).reduce(initial) do |accumulator, index|
|
|
23
|
+
accumulator.public_send(operation, body_value(name, index))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :evaluator, :lower, :upper, :body
|
|
30
|
+
|
|
31
|
+
# Parses `i=1` style lower bounds into the index name and its start.
|
|
32
|
+
def index_definition
|
|
33
|
+
tokens = lower.is_a?(Formula) ? lower.value : Array(lower)
|
|
34
|
+
index = tokens.first
|
|
35
|
+
if index.is_a?(Core) && index.reserved_constant
|
|
36
|
+
evaluator.unsupported("reserved constant as iteration index")
|
|
37
|
+
end
|
|
38
|
+
name = index.is_a?(Core) ? index.variable_name : nil
|
|
39
|
+
unless name && tokens[1].is_a?(Symbols::Equal)
|
|
40
|
+
evaluator.unsupported("malformed iteration bounds")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[name, evaluator.evaluate_nodes(tokens[2..])]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_bounds(from, to)
|
|
47
|
+
unless from.is_a?(Integer) && to.is_a?(Integer)
|
|
48
|
+
raise Errors::Evaluation::MathDomainError, "iteration bounds must be integers"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
limit = Plurimath.configuration.evaluation_max_iterations
|
|
52
|
+
return if limit.nil? || (to - from + 1) <= limit
|
|
53
|
+
|
|
54
|
+
evaluator.unsupported("iteration range larger than #{limit} steps")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def body_value(name, index)
|
|
58
|
+
evaluator.with_binding(name, index) { evaluator.evaluate_node(body) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plurimath/errors/evaluation"
|
|
4
|
+
|
|
5
|
+
module Plurimath
|
|
6
|
+
module Math
|
|
7
|
+
module Evaluation
|
|
8
|
+
autoload :Evaluator, "#{__dir__}/evaluation/evaluator"
|
|
9
|
+
autoload :ExpressionParser, "#{__dir__}/evaluation/expression_parser"
|
|
10
|
+
autoload :Iteration, "#{__dir__}/evaluation/iteration"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -54,6 +54,15 @@ module Plurimath
|
|
|
54
54
|
object.left_right_wrapper == left_right_wrapper
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def evaluate(bindings = {})
|
|
58
|
+
hash = bindings.to_hash if bindings.respond_to?(:to_hash)
|
|
59
|
+
unless hash.is_a?(Hash)
|
|
60
|
+
raise ArgumentError, "bindings must be a Hash-like object"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Evaluation::Evaluator.new(self, hash).evaluate
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
def to_asciimath(formatter: nil, unitsml: {}, options: nil)
|
|
58
67
|
options ||= { formatter: formatter, unitsml: unitsml }.compact
|
|
59
68
|
options[:formula] ||= self
|
|
@@ -39,6 +39,10 @@ module Plurimath
|
|
|
39
39
|
"#{first_value}#{parameter_one&.to_unicodemath(options: options)}#{second_value}"
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def evaluate(evaluator)
|
|
43
|
+
evaluator.evaluate_node(parameter_one).ceil
|
|
44
|
+
end
|
|
45
|
+
|
|
42
46
|
def line_breaking(obj)
|
|
43
47
|
parameter_one.line_breaking(obj)
|
|
44
48
|
if obj.value_exist?
|
|
@@ -8,6 +8,10 @@ module Plurimath
|
|
|
8
8
|
false
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def evaluate(evaluator)
|
|
12
|
+
::Math.exp(evaluator.evaluate_node(parameter_one))
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def to_omml_without_math_tag(display_style, options:)
|
|
12
16
|
array = []
|
|
13
17
|
array << r_element("exp", rpr_tag: false) unless hide_function_name
|
|
@@ -116,6 +116,10 @@ module Plurimath
|
|
|
116
116
|
"#{unicode_open_paren(options: options)}#{fenced_value}#{unicode_close_paren(options: options)}"
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
def evaluate(evaluator)
|
|
120
|
+
evaluator.evaluate_nodes(parameter_two)
|
|
121
|
+
end
|
|
122
|
+
|
|
119
123
|
def to_asciimath_math_zone(spacing, last = false, indent = true,
|
|
120
124
|
options:)
|
|
121
125
|
filtered_values(parameter_two,
|
|
@@ -36,6 +36,10 @@ module Plurimath
|
|
|
36
36
|
"#{first_value}#{parameter_one&.to_unicodemath(options: options)}#{second_value}"
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def evaluate(evaluator)
|
|
40
|
+
evaluator.evaluate_node(parameter_one).floor
|
|
41
|
+
end
|
|
42
|
+
|
|
39
43
|
def line_breaking(obj)
|
|
40
44
|
parameter_one.line_breaking(obj)
|
|
41
45
|
if obj.value_exist?
|