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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +311 -58
  4. data/Gemfile +3 -2
  5. data/README.adoc +331 -15
  6. data/lib/plurimath/asciimath/parse.rb +6 -1
  7. data/lib/plurimath/asciimath/transform.rb +2 -0
  8. data/lib/plurimath/base_number_prefix.rb +43 -0
  9. data/lib/plurimath/configuration.rb +9 -1
  10. data/lib/plurimath/deprecation.rb +2 -1
  11. data/lib/plurimath/errors/configuration_error.rb +5 -1
  12. data/lib/plurimath/errors/deprecation_error.rb +2 -1
  13. data/lib/plurimath/errors/evaluation/division_by_zero_error.rb +13 -0
  14. data/lib/plurimath/errors/evaluation/error.rb +9 -0
  15. data/lib/plurimath/errors/evaluation/invalid_binding_error.rb +14 -0
  16. data/lib/plurimath/errors/evaluation/invalid_binding_key_error.rb +14 -0
  17. data/lib/plurimath/errors/evaluation/math_domain_error.rb +9 -0
  18. data/lib/plurimath/errors/evaluation/missing_variable_error.rb +13 -0
  19. data/lib/plurimath/errors/evaluation/non_finite_result_error.rb +13 -0
  20. data/lib/plurimath/errors/evaluation/unsupported_expression_error.rb +13 -0
  21. data/lib/plurimath/errors/evaluation.rb +18 -0
  22. data/lib/plurimath/errors/invalid_number.rb +17 -0
  23. data/lib/plurimath/errors/unsupported_base.rb +18 -0
  24. data/lib/plurimath/errors/{formatter/unsupported_locale.rb → unsupported_locale.rb} +1 -1
  25. data/lib/plurimath/errors.rb +9 -0
  26. data/lib/plurimath/formatter/numbers/base_notation.rb +55 -27
  27. data/lib/plurimath/formatter/numbers/format_options.rb +123 -17
  28. data/lib/plurimath/formatter/numbers/formatted_notation.rb +62 -0
  29. data/lib/plurimath/formatter/numbers/formatted_number.rb +87 -0
  30. data/lib/plurimath/formatter/numbers/fraction.rb +8 -17
  31. data/lib/plurimath/formatter/numbers/integer.rb +3 -0
  32. data/lib/plurimath/formatter/numbers/mathml_renderer.rb +56 -0
  33. data/lib/plurimath/formatter/numbers/notation_renderer.rb +58 -28
  34. data/lib/plurimath/formatter/numbers/number_renderer.rb +26 -13
  35. data/lib/plurimath/formatter/numbers/omml_renderer.rb +74 -0
  36. data/lib/plurimath/formatter/numbers/precision_resolver.rb +16 -3
  37. data/lib/plurimath/formatter/numbers/sign_renderer.rb +2 -1
  38. data/lib/plurimath/formatter/numbers/significant.rb +4 -2
  39. data/lib/plurimath/formatter/numbers/source.rb +46 -6
  40. data/lib/plurimath/formatter/numbers/text_renderer.rb +52 -0
  41. data/lib/plurimath/formatter/numbers.rb +6 -2
  42. data/lib/plurimath/formatter/standard.rb +6 -0
  43. data/lib/plurimath/formatter/supported_locales.rb +1 -1
  44. data/lib/plurimath/formatter.rb +0 -2
  45. data/lib/plurimath/html/parse.rb +5 -0
  46. data/lib/plurimath/html/transform.rb +26 -12
  47. data/lib/plurimath/latex/parse.rb +5 -0
  48. data/lib/plurimath/latex/transform.rb +2 -0
  49. data/lib/plurimath/math/core.rb +69 -14
  50. data/lib/plurimath/math/evaluation/evaluator.rb +147 -0
  51. data/lib/plurimath/math/evaluation/expression_parser.rb +215 -0
  52. data/lib/plurimath/math/evaluation/iteration.rb +63 -0
  53. data/lib/plurimath/math/evaluation.rb +13 -0
  54. data/lib/plurimath/math/formula.rb +9 -0
  55. data/lib/plurimath/math/function/abs.rb +4 -0
  56. data/lib/plurimath/math/function/arccos.rb +4 -0
  57. data/lib/plurimath/math/function/arcsin.rb +4 -0
  58. data/lib/plurimath/math/function/arctan.rb +4 -0
  59. data/lib/plurimath/math/function/ceil.rb +4 -0
  60. data/lib/plurimath/math/function/cos.rb +4 -0
  61. data/lib/plurimath/math/function/cosh.rb +4 -0
  62. data/lib/plurimath/math/function/cot.rb +4 -0
  63. data/lib/plurimath/math/function/coth.rb +4 -0
  64. data/lib/plurimath/math/function/csc.rb +4 -0
  65. data/lib/plurimath/math/function/csch.rb +4 -0
  66. data/lib/plurimath/math/function/exp.rb +4 -0
  67. data/lib/plurimath/math/function/fenced.rb +4 -0
  68. data/lib/plurimath/math/function/floor.rb +4 -0
  69. data/lib/plurimath/math/function/frac.rb +7 -0
  70. data/lib/plurimath/math/function/gcd.rb +9 -0
  71. data/lib/plurimath/math/function/lcm.rb +9 -0
  72. data/lib/plurimath/math/function/lg.rb +4 -0
  73. data/lib/plurimath/math/function/ln.rb +4 -0
  74. data/lib/plurimath/math/function/log.rb +19 -0
  75. data/lib/plurimath/math/function/max.rb +4 -0
  76. data/lib/plurimath/math/function/min.rb +4 -0
  77. data/lib/plurimath/math/function/mod.rb +15 -0
  78. data/lib/plurimath/math/function/overleftrightarrow.rb +2 -2
  79. data/lib/plurimath/math/function/power.rb +10 -0
  80. data/lib/plurimath/math/function/prod.rb +10 -0
  81. data/lib/plurimath/math/function/root.rb +7 -0
  82. data/lib/plurimath/math/function/sec.rb +4 -0
  83. data/lib/plurimath/math/function/sech.rb +4 -0
  84. data/lib/plurimath/math/function/sin.rb +4 -0
  85. data/lib/plurimath/math/function/sinh.rb +4 -0
  86. data/lib/plurimath/math/function/sqrt.rb +4 -0
  87. data/lib/plurimath/math/function/sum.rb +10 -0
  88. data/lib/plurimath/math/function/tan.rb +4 -0
  89. data/lib/plurimath/math/function/tanh.rb +4 -0
  90. data/lib/plurimath/math/function/text.rb +17 -0
  91. data/lib/plurimath/math/number.rb +44 -23
  92. data/lib/plurimath/math/symbols/cdot.rb +4 -0
  93. data/lib/plurimath/math/symbols/div.rb +4 -0
  94. data/lib/plurimath/math/symbols/hat.rb +4 -0
  95. data/lib/plurimath/math/symbols/minus.rb +4 -0
  96. data/lib/plurimath/math/symbols/pi.rb +5 -1
  97. data/lib/plurimath/math/symbols/plus.rb +4 -0
  98. data/lib/plurimath/math/symbols/slash.rb +4 -0
  99. data/lib/plurimath/math/symbols/symbol.rb +45 -0
  100. data/lib/plurimath/math/symbols/times.rb +4 -0
  101. data/lib/plurimath/math.rb +5 -1
  102. data/lib/plurimath/mathml/constants.rb +18 -0
  103. data/lib/plurimath/number_formatter.rb +63 -3
  104. data/lib/plurimath/omml/formula_transformation.rb +2 -1
  105. data/lib/plurimath/omml/translator.rb +4 -1
  106. data/lib/plurimath/setup/opal.rb.erb +13 -0
  107. data/lib/plurimath/unicode_math/parse.rb +98 -54
  108. data/lib/plurimath/unicode_math/parser.rb +7 -4
  109. data/lib/plurimath/unicode_math/parsing_rules/absence_rules.rb +33 -21
  110. data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +28 -16
  111. data/lib/plurimath/unicode_math/parsing_rules/constants_rules.rb +18 -7
  112. data/lib/plurimath/unicode_math/parsing_rules/masked.rb +35 -15
  113. data/lib/plurimath/unicode_math/parsing_rules/sub_sup.rb +98 -72
  114. data/lib/plurimath/unicode_math/transform.rb +1 -0
  115. data/lib/plurimath/utility.rb +1 -1
  116. data/lib/plurimath/version.rb +1 -1
  117. data/lib/plurimath.rb +2 -0
  118. metadata +24 -5
  119. data/lib/plurimath/errors/formatter/unsupported_base.rb +0 -21
  120. 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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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, sup_value: sup_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)) }
@@ -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:, namespace: "m")
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 { |object|
62
- object.insert_t_tag(display_style, options: options)
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
- updated_object_values(variable, obj: obj,
264
- update_value: true) if obj.value_exist?
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?("&#x27;")
363
418
 
364
- Utility.primes_constants.any? { |_prefix, prime|
365
- unicodemath_field_value(field).include?(prime)
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