plurimath 0.11.0 → 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 +311 -58
- data/Gemfile +3 -2
- data/README.adoc +331 -15
- 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/deprecation.rb +2 -1
- data/lib/plurimath/errors/configuration_error.rb +5 -1
- data/lib/plurimath/errors/deprecation_error.rb +2 -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/invalid_number.rb +17 -0
- data/lib/plurimath/errors/unsupported_base.rb +18 -0
- data/lib/plurimath/errors/{formatter/unsupported_locale.rb → unsupported_locale.rb} +1 -1
- data/lib/plurimath/errors.rb +9 -0
- data/lib/plurimath/formatter/numbers/base_notation.rb +55 -27
- data/lib/plurimath/formatter/numbers/format_options.rb +123 -17
- 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 +8 -17
- data/lib/plurimath/formatter/numbers/integer.rb +3 -0
- data/lib/plurimath/formatter/numbers/mathml_renderer.rb +56 -0
- data/lib/plurimath/formatter/numbers/notation_renderer.rb +58 -28
- data/lib/plurimath/formatter/numbers/number_renderer.rb +26 -13
- data/lib/plurimath/formatter/numbers/omml_renderer.rb +74 -0
- data/lib/plurimath/formatter/numbers/precision_resolver.rb +16 -3
- data/lib/plurimath/formatter/numbers/sign_renderer.rb +2 -1
- data/lib/plurimath/formatter/numbers/significant.rb +4 -2
- data/lib/plurimath/formatter/numbers/source.rb +46 -6
- data/lib/plurimath/formatter/numbers/text_renderer.rb +52 -0
- data/lib/plurimath/formatter/numbers.rb +6 -2
- data/lib/plurimath/formatter/standard.rb +6 -0
- data/lib/plurimath/formatter/supported_locales.rb +1 -1
- data/lib/plurimath/formatter.rb +0 -2
- data/lib/plurimath/html/parse.rb +5 -0
- data/lib/plurimath/html/transform.rb +26 -12
- data/lib/plurimath/latex/parse.rb +5 -0
- data/lib/plurimath/latex/transform.rb +2 -0
- data/lib/plurimath/math/core.rb +69 -14
- 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/overleftrightarrow.rb +2 -2
- 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 +44 -23
- 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 +5 -1
- data/lib/plurimath/mathml/constants.rb +18 -0
- data/lib/plurimath/number_formatter.rb +63 -3
- data/lib/plurimath/omml/formula_transformation.rb +2 -1
- data/lib/plurimath/omml/translator.rb +4 -1
- data/lib/plurimath/setup/opal.rb.erb +13 -0
- data/lib/plurimath/unicode_math/parse.rb +98 -54
- data/lib/plurimath/unicode_math/parser.rb +7 -4
- data/lib/plurimath/unicode_math/parsing_rules/absence_rules.rb +33 -21
- data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +28 -16
- data/lib/plurimath/unicode_math/parsing_rules/constants_rules.rb +18 -7
- data/lib/plurimath/unicode_math/parsing_rules/masked.rb +35 -15
- data/lib/plurimath/unicode_math/parsing_rules/sub_sup.rb +98 -72
- data/lib/plurimath/unicode_math/transform.rb +1 -0
- data/lib/plurimath/utility.rb +1 -1
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +2 -0
- metadata +24 -5
- data/lib/plurimath/errors/formatter/unsupported_base.rb +0 -21
- data/lib/plurimath/formatter/numbers/parts_renderer.rb +0 -30
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
class Html
|
|
5
5
|
class Transform < Parslet::Transform
|
|
6
|
+
include Plurimath::BaseNumberPrefix::Transform
|
|
7
|
+
|
|
6
8
|
rule(text: simple(:text)) { Math::Function::Text.new(text) }
|
|
7
9
|
rule(unary: simple(:unary)) { Utility.get_class(unary).new }
|
|
8
10
|
rule(symbol: simple(:symbol)) { TransformUtility.symbol(symbol) }
|
|
@@ -202,25 +204,29 @@ module Plurimath
|
|
|
202
204
|
rule(sub_sup: simple(:sub_sup),
|
|
203
205
|
sub_value: simple(:sub_value),
|
|
204
206
|
sup_value: simple(:sup_value)) do
|
|
205
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
207
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
208
|
+
sup_value: sup_value)
|
|
206
209
|
end
|
|
207
210
|
|
|
208
211
|
rule(sub_sup: simple(:sub_sup),
|
|
209
212
|
sub_value: simple(:sub_value),
|
|
210
213
|
sup_value: sequence(:sup_value)) do
|
|
211
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
214
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
215
|
+
sup_value: sup_value)
|
|
212
216
|
end
|
|
213
217
|
|
|
214
218
|
rule(sub_sup: simple(:sub_sup),
|
|
215
219
|
sub_value: sequence(:sub_value),
|
|
216
220
|
sup_value: simple(:sup_value)) do
|
|
217
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
221
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
222
|
+
sup_value: sup_value)
|
|
218
223
|
end
|
|
219
224
|
|
|
220
225
|
rule(sub_sup: simple(:sub_sup),
|
|
221
226
|
sub_value: sequence(:sub_value),
|
|
222
227
|
sup_value: sequence(:sup_value)) do
|
|
223
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
228
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
229
|
+
sup_value: sup_value)
|
|
224
230
|
end
|
|
225
231
|
|
|
226
232
|
# Parslet emits separate shapes for each sub/sup value combination and
|
|
@@ -303,7 +309,8 @@ module Plurimath
|
|
|
303
309
|
sup_value: simple(:sup_value),
|
|
304
310
|
expression: simple(:expression)) do
|
|
305
311
|
TransformUtility.append_expression(
|
|
306
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
312
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
313
|
+
sup_value: sup_value),
|
|
307
314
|
expression,
|
|
308
315
|
)
|
|
309
316
|
end
|
|
@@ -313,7 +320,8 @@ module Plurimath
|
|
|
313
320
|
sup_value: simple(:sup_value),
|
|
314
321
|
expression: sequence(:expression)) do
|
|
315
322
|
TransformUtility.append_expression(
|
|
316
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
323
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
324
|
+
sup_value: sup_value),
|
|
317
325
|
expression,
|
|
318
326
|
)
|
|
319
327
|
end
|
|
@@ -323,7 +331,8 @@ module Plurimath
|
|
|
323
331
|
sup_value: sequence(:sup_value),
|
|
324
332
|
expression: simple(:expression)) do
|
|
325
333
|
TransformUtility.append_expression(
|
|
326
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
334
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
335
|
+
sup_value: sup_value),
|
|
327
336
|
expression,
|
|
328
337
|
)
|
|
329
338
|
end
|
|
@@ -333,7 +342,8 @@ module Plurimath
|
|
|
333
342
|
sup_value: sequence(:sup_value),
|
|
334
343
|
expression: sequence(:expression)) do
|
|
335
344
|
TransformUtility.append_expression(
|
|
336
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
345
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
346
|
+
sup_value: sup_value),
|
|
337
347
|
expression,
|
|
338
348
|
)
|
|
339
349
|
end
|
|
@@ -343,7 +353,8 @@ module Plurimath
|
|
|
343
353
|
sup_value: simple(:sup_value),
|
|
344
354
|
expression: simple(:expression)) do
|
|
345
355
|
TransformUtility.append_expression(
|
|
346
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
356
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
357
|
+
sup_value: sup_value),
|
|
347
358
|
expression,
|
|
348
359
|
)
|
|
349
360
|
end
|
|
@@ -353,7 +364,8 @@ module Plurimath
|
|
|
353
364
|
sup_value: simple(:sup_value),
|
|
354
365
|
expression: sequence(:expression)) do
|
|
355
366
|
TransformUtility.append_expression(
|
|
356
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
367
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
368
|
+
sup_value: sup_value),
|
|
357
369
|
expression,
|
|
358
370
|
)
|
|
359
371
|
end
|
|
@@ -363,7 +375,8 @@ module Plurimath
|
|
|
363
375
|
sup_value: sequence(:sup_value),
|
|
364
376
|
expression: simple(:expression)) do
|
|
365
377
|
TransformUtility.append_expression(
|
|
366
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
378
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
379
|
+
sup_value: sup_value),
|
|
367
380
|
expression,
|
|
368
381
|
)
|
|
369
382
|
end
|
|
@@ -373,7 +386,8 @@ module Plurimath
|
|
|
373
386
|
sup_value: sequence(:sup_value),
|
|
374
387
|
expression: sequence(:expression)) do
|
|
375
388
|
TransformUtility.append_expression(
|
|
376
|
-
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
389
|
+
TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
|
|
390
|
+
sup_value: sup_value),
|
|
377
391
|
expression,
|
|
378
392
|
)
|
|
379
393
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
class Latex
|
|
5
5
|
class Parse < Parslet::Parser
|
|
6
|
+
include Plurimath::BaseNumberPrefix::Parser
|
|
7
|
+
|
|
6
8
|
rule(:base) { str("_") }
|
|
7
9
|
rule(:power) { str("^") }
|
|
8
10
|
rule(:slash) { str("\\") }
|
|
@@ -101,6 +103,9 @@ module Plurimath
|
|
|
101
103
|
(rparen.absent? >> symbol_class_commands) |
|
|
102
104
|
(slash >> math_operators_classes) |
|
|
103
105
|
match["a-zA-Z"].as(:symbols) |
|
|
106
|
+
hex_number |
|
|
107
|
+
binary_number |
|
|
108
|
+
octal_number |
|
|
104
109
|
(match["0-9"].repeat(0) >> decimal_marker.maybe >> match["0-9"].repeat(1)).as(:number) |
|
|
105
110
|
match["0-9"].repeat(1).as(:number) |
|
|
106
111
|
(str("\\\\").as("\\\\") >> match(/\s/).repeat) |
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
class Latex
|
|
5
5
|
class Transform < Parslet::Transform
|
|
6
|
+
include Plurimath::BaseNumberPrefix::Transform
|
|
7
|
+
|
|
6
8
|
rule(base: simple(:base)) { base }
|
|
7
9
|
rule(over: simple(:over)) { over }
|
|
8
10
|
rule(number: simple(:num)) { Math::Number.new(Utility.html_entity_to_unicode(num.to_s)) }
|
data/lib/plurimath/math/core.rb
CHANGED
|
@@ -53,14 +53,15 @@ module Plurimath
|
|
|
53
53
|
wrapper_tag << r_tag
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def omml_parameter(field, display_style, tag_name:, options:,
|
|
56
|
+
def omml_parameter(field, display_style, tag_name:, options:,
|
|
57
|
+
namespace: "m")
|
|
57
58
|
tag = ox_element(tag_name, namespace: namespace)
|
|
58
59
|
return empty_tag(tag) unless field
|
|
59
60
|
|
|
60
61
|
field_value = if field.is_a?(Array)
|
|
61
|
-
field.map
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
field.map do |object|
|
|
63
|
+
object.insert_t_tag(display_style, options: options)
|
|
64
|
+
end
|
|
64
65
|
else
|
|
65
66
|
field.insert_t_tag(display_style, options: options)
|
|
66
67
|
end
|
|
@@ -106,7 +107,7 @@ module Plurimath
|
|
|
106
107
|
|
|
107
108
|
options[:array] << field&.to_asciimath_math_zone(
|
|
108
109
|
hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
|
|
109
|
-
)
|
|
110
|
+
)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def latex_fields_to_print(field, options = {})
|
|
@@ -131,7 +132,7 @@ module Plurimath
|
|
|
131
132
|
|
|
132
133
|
options[:array] << field&.to_mathml_math_zone(
|
|
133
134
|
hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
|
|
134
|
-
)
|
|
135
|
+
)
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
def omml_fields_to_print(field, options = {})
|
|
@@ -156,13 +157,13 @@ module Plurimath
|
|
|
156
157
|
|
|
157
158
|
options[:array] << field&.to_unicodemath_math_zone(
|
|
158
159
|
hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
|
|
159
|
-
)
|
|
160
|
+
)
|
|
160
161
|
end
|
|
161
162
|
|
|
162
163
|
def dump_mathml(field, intent = false, options:)
|
|
163
164
|
dump_ox_nodes(field.mathml_nodes(intent, options: options)).gsub(
|
|
164
165
|
/\n\s*/, ""
|
|
165
|
-
)
|
|
166
|
+
)
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def dump_omml(field, display_style, options:)
|
|
@@ -170,7 +171,7 @@ module Plurimath
|
|
|
170
171
|
|
|
171
172
|
dump_ox_nodes(field.omml_nodes(display_style, options: options)).gsub(
|
|
172
173
|
/\n\s*/, ""
|
|
173
|
-
)
|
|
174
|
+
)
|
|
174
175
|
end
|
|
175
176
|
|
|
176
177
|
def mathml_nodes(intent, options:)
|
|
@@ -260,8 +261,10 @@ module Plurimath
|
|
|
260
261
|
case field
|
|
261
262
|
when Core
|
|
262
263
|
field.line_breaking(obj)
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
if obj.value_exist?
|
|
265
|
+
updated_object_values(variable, obj: obj,
|
|
266
|
+
update_value: true)
|
|
267
|
+
end
|
|
265
268
|
when Array
|
|
266
269
|
array_line_break_field(field, variable, obj)
|
|
267
270
|
end
|
|
@@ -350,6 +353,58 @@ module Plurimath
|
|
|
350
353
|
false
|
|
351
354
|
end
|
|
352
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
|
+
|
|
353
408
|
def unicodemath_parens(field, options:)
|
|
354
409
|
paren = field.to_unicodemath(options: options)
|
|
355
410
|
return paren if field.is_a?(Math::Function::Fenced)
|
|
@@ -361,9 +416,9 @@ module Plurimath
|
|
|
361
416
|
return false unless field.is_a?(Math::Symbols::Symbol)
|
|
362
417
|
return true if field&.value&.include?("'")
|
|
363
418
|
|
|
364
|
-
Utility.primes_constants.any?
|
|
365
|
-
|
|
366
|
-
|
|
419
|
+
Utility.primes_constants.any? do |_prefix, prime|
|
|
420
|
+
unicodemath_field_value(field).include?(prime)
|
|
421
|
+
end
|
|
367
422
|
end
|
|
368
423
|
|
|
369
424
|
def paren?
|
|
@@ -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
|