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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +108 -175
  4. data/Gemfile +1 -0
  5. data/README.adoc +282 -6
  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/errors/evaluation/division_by_zero_error.rb +13 -0
  11. data/lib/plurimath/errors/evaluation/error.rb +9 -0
  12. data/lib/plurimath/errors/evaluation/invalid_binding_error.rb +14 -0
  13. data/lib/plurimath/errors/evaluation/invalid_binding_key_error.rb +14 -0
  14. data/lib/plurimath/errors/evaluation/math_domain_error.rb +9 -0
  15. data/lib/plurimath/errors/evaluation/missing_variable_error.rb +13 -0
  16. data/lib/plurimath/errors/evaluation/non_finite_result_error.rb +13 -0
  17. data/lib/plurimath/errors/evaluation/unsupported_expression_error.rb +13 -0
  18. data/lib/plurimath/errors/evaluation.rb +18 -0
  19. data/lib/plurimath/errors.rb +1 -0
  20. data/lib/plurimath/formatter/numbers/base_notation.rb +54 -31
  21. data/lib/plurimath/formatter/numbers/formatted_notation.rb +62 -0
  22. data/lib/plurimath/formatter/numbers/formatted_number.rb +87 -0
  23. data/lib/plurimath/formatter/numbers/fraction.rb +1 -1
  24. data/lib/plurimath/formatter/numbers/mathml_renderer.rb +56 -0
  25. data/lib/plurimath/formatter/numbers/notation_renderer.rb +30 -29
  26. data/lib/plurimath/formatter/numbers/number_renderer.rb +10 -9
  27. data/lib/plurimath/formatter/numbers/omml_renderer.rb +74 -0
  28. data/lib/plurimath/formatter/numbers/source.rb +29 -4
  29. data/lib/plurimath/formatter/numbers/text_renderer.rb +52 -0
  30. data/lib/plurimath/formatter/numbers.rb +6 -2
  31. data/lib/plurimath/html/parse.rb +5 -0
  32. data/lib/plurimath/html/transform.rb +2 -0
  33. data/lib/plurimath/latex/parse.rb +5 -0
  34. data/lib/plurimath/latex/transform.rb +2 -0
  35. data/lib/plurimath/math/core.rb +52 -0
  36. data/lib/plurimath/math/evaluation/evaluator.rb +147 -0
  37. data/lib/plurimath/math/evaluation/expression_parser.rb +215 -0
  38. data/lib/plurimath/math/evaluation/iteration.rb +63 -0
  39. data/lib/plurimath/math/evaluation.rb +13 -0
  40. data/lib/plurimath/math/formula.rb +9 -0
  41. data/lib/plurimath/math/function/abs.rb +4 -0
  42. data/lib/plurimath/math/function/arccos.rb +4 -0
  43. data/lib/plurimath/math/function/arcsin.rb +4 -0
  44. data/lib/plurimath/math/function/arctan.rb +4 -0
  45. data/lib/plurimath/math/function/ceil.rb +4 -0
  46. data/lib/plurimath/math/function/cos.rb +4 -0
  47. data/lib/plurimath/math/function/cosh.rb +4 -0
  48. data/lib/plurimath/math/function/cot.rb +4 -0
  49. data/lib/plurimath/math/function/coth.rb +4 -0
  50. data/lib/plurimath/math/function/csc.rb +4 -0
  51. data/lib/plurimath/math/function/csch.rb +4 -0
  52. data/lib/plurimath/math/function/exp.rb +4 -0
  53. data/lib/plurimath/math/function/fenced.rb +4 -0
  54. data/lib/plurimath/math/function/floor.rb +4 -0
  55. data/lib/plurimath/math/function/frac.rb +7 -0
  56. data/lib/plurimath/math/function/gcd.rb +9 -0
  57. data/lib/plurimath/math/function/lcm.rb +9 -0
  58. data/lib/plurimath/math/function/lg.rb +4 -0
  59. data/lib/plurimath/math/function/ln.rb +4 -0
  60. data/lib/plurimath/math/function/log.rb +19 -0
  61. data/lib/plurimath/math/function/max.rb +4 -0
  62. data/lib/plurimath/math/function/min.rb +4 -0
  63. data/lib/plurimath/math/function/mod.rb +15 -0
  64. data/lib/plurimath/math/function/power.rb +10 -0
  65. data/lib/plurimath/math/function/prod.rb +10 -0
  66. data/lib/plurimath/math/function/root.rb +7 -0
  67. data/lib/plurimath/math/function/sec.rb +4 -0
  68. data/lib/plurimath/math/function/sech.rb +4 -0
  69. data/lib/plurimath/math/function/sin.rb +4 -0
  70. data/lib/plurimath/math/function/sinh.rb +4 -0
  71. data/lib/plurimath/math/function/sqrt.rb +4 -0
  72. data/lib/plurimath/math/function/sum.rb +10 -0
  73. data/lib/plurimath/math/function/tan.rb +4 -0
  74. data/lib/plurimath/math/function/tanh.rb +4 -0
  75. data/lib/plurimath/math/function/text.rb +17 -0
  76. data/lib/plurimath/math/number.rb +40 -29
  77. data/lib/plurimath/math/symbols/cdot.rb +4 -0
  78. data/lib/plurimath/math/symbols/div.rb +4 -0
  79. data/lib/plurimath/math/symbols/hat.rb +4 -0
  80. data/lib/plurimath/math/symbols/minus.rb +4 -0
  81. data/lib/plurimath/math/symbols/pi.rb +5 -1
  82. data/lib/plurimath/math/symbols/plus.rb +4 -0
  83. data/lib/plurimath/math/symbols/slash.rb +4 -0
  84. data/lib/plurimath/math/symbols/symbol.rb +45 -0
  85. data/lib/plurimath/math/symbols/times.rb +4 -0
  86. data/lib/plurimath/math.rb +1 -0
  87. data/lib/plurimath/mathml/constants.rb +18 -0
  88. data/lib/plurimath/number_formatter.rb +47 -28
  89. data/lib/plurimath/setup/opal.rb.erb +13 -0
  90. data/lib/plurimath/unicode_math/parse.rb +5 -1
  91. data/lib/plurimath/unicode_math/transform.rb +469 -755
  92. data/lib/plurimath/utility.rb +1 -1
  93. data/lib/plurimath/version.rb +1 -1
  94. data/lib/plurimath.rb +1 -0
  95. metadata +21 -3
  96. data/lib/plurimath/formatter/numbers/parts_renderer.rb +0 -30
@@ -89,6 +89,13 @@ module Plurimath
89
89
  "#{first_value}∕#{second_value}" if self.options&.key?(:ldiv)
90
90
  end
91
91
 
92
+ def evaluate(evaluator)
93
+ evaluator.divide(
94
+ evaluator.evaluate_node(parameter_one),
95
+ evaluator.evaluate_node(parameter_two),
96
+ )
97
+ end
98
+
92
99
  def line_breaking(obj)
93
100
  parameter_one&.line_breaking(obj)
94
101
  if obj.value_exist?
@@ -8,6 +8,15 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ values = evaluator.function_arguments(parameter_one)
13
+ unless values.all?(Integer)
14
+ raise Errors::Evaluation::MathDomainError, "gcd requires integer arguments"
15
+ end
16
+
17
+ values.reduce(:gcd)
18
+ end
19
+
11
20
  def to_omml_without_math_tag(display_style, options:)
12
21
  array = []
13
22
  array << r_element("gcd", rpr_tag: false) unless hide_function_name
@@ -8,6 +8,15 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ values = evaluator.function_arguments(parameter_one)
13
+ unless values.all?(Integer)
14
+ raise Errors::Evaluation::MathDomainError, "lcm requires integer arguments"
15
+ end
16
+
17
+ values.reduce(:lcm)
18
+ end
19
+
11
20
  def to_asciimath(options:)
12
21
  first_value = " #{asciimath_value(options: options)}" if parameter_one
13
22
  "lcm#{first_value}"
@@ -8,6 +8,10 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ ::Math.log10(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("lg", rpr_tag: false) unless hide_function_name
@@ -8,6 +8,10 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ ::Math.log(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("ln", rpr_tag: false) unless hide_function_name
@@ -10,6 +10,25 @@ module Plurimath
10
10
  second_value: "supscript",
11
11
  }.freeze
12
12
 
13
+ # `log` carries its base/exponent but not its argument; the parser
14
+ # binds the following fenced group via #evaluate_with_argument, so a
15
+ # bare `log` is unsupported.
16
+ def evaluate(evaluator)
17
+ evaluator.unsupported(self)
18
+ end
19
+
20
+ def evaluate_with_argument(evaluator, argument)
21
+ base = parameter_one ? evaluator.evaluate_node(parameter_one) : 10
22
+ unless base.positive? && base != 1
23
+ raise Errors::Evaluation::MathDomainError,
24
+ "log base must be a positive number other than 1"
25
+ end
26
+
27
+ result = ::Math.log(evaluator.evaluate_node(argument), base)
28
+ result = evaluator.power(result, evaluator.evaluate_node(parameter_two)) if parameter_two
29
+ result
30
+ end
31
+
13
32
  def to_asciimath(options:)
14
33
  if parameter_one
15
34
  first_value = "_#{wrapped(parameter_one,
@@ -8,6 +8,10 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ evaluator.function_arguments(parameter_one).max
13
+ end
14
+
11
15
  def to_omml_without_math_tag(display_style, options:)
12
16
  array = []
13
17
  array << r_element("max", rpr_tag: false) unless hide_function_name
@@ -8,6 +8,10 @@ module Plurimath
8
8
  false
9
9
  end
10
10
 
11
+ def evaluate(evaluator)
12
+ evaluator.function_arguments(parameter_one).min
13
+ end
14
+
11
15
  def to_omml_without_math_tag(display_style, options:)
12
16
  array = []
13
17
  array << r_element("min", rpr_tag: false) unless hide_function_name
@@ -10,6 +10,21 @@ module Plurimath
10
10
  second_value: "argument",
11
11
  }.freeze
12
12
 
13
+ def evaluate(evaluator)
14
+ evaluator.modulo(
15
+ evaluator.evaluate_node(parameter_one),
16
+ evaluator.evaluate_node(parameter_two),
17
+ )
18
+ end
19
+
20
+ # `-a mod b` negates the dividend, matching `(-a) mod b`.
21
+ def evaluate_negated(evaluator)
22
+ evaluator.modulo(
23
+ -evaluator.evaluate_node(parameter_one),
24
+ evaluator.evaluate_node(parameter_two),
25
+ )
26
+ end
27
+
13
28
  def to_asciimath(options:)
14
29
  first_value = parameter_one&.to_asciimath(options: options)
15
30
  second_value = parameter_two&.to_asciimath(options: options)
@@ -80,6 +80,16 @@ module Plurimath
80
80
  end
81
81
  end
82
82
 
83
+ # Nested Power trees evaluate literally: `Power(Power(2, 3), 2)` is
84
+ # `(2^3)^2`, whether it came from explicit MathML nesting or a
85
+ # left-nested source chain.
86
+ def evaluate(evaluator)
87
+ evaluator.power(
88
+ evaluator.evaluate_node(parameter_one),
89
+ evaluator.evaluate_node(parameter_two),
90
+ )
91
+ end
92
+
83
93
  def line_breaking(obj)
84
94
  parameter_one&.line_breaking(obj)
85
95
  if obj.value_exist?
@@ -12,6 +12,16 @@ module Plurimath
12
12
  second_value: "supscript",
13
13
  }.freeze
14
14
 
15
+ def evaluate(evaluator)
16
+ if parameter_one.nil? || parameter_two.nil? || parameter_three.nil?
17
+ evaluator.unsupported(self)
18
+ end
19
+
20
+ Evaluation::Iteration
21
+ .new(evaluator, parameter_one, parameter_two, parameter_three)
22
+ .accumulate(1, :*)
23
+ end
24
+
15
25
  def initialize(parameter_one = nil,
16
26
  parameter_two = nil,
17
27
  parameter_three = nil,
@@ -51,6 +51,13 @@ module Plurimath
51
51
  second_value = parameter_two.to_unicodemath(options: options) if parameter_two
52
52
  "√(#{first_value}&#{second_value})"
53
53
  end
54
+
55
+ def evaluate(evaluator)
56
+ evaluator.power(
57
+ evaluator.evaluate_node(parameter_two),
58
+ evaluator.divide(1.0, evaluator.evaluate_node(parameter_one)),
59
+ )
60
+ end
54
61
  end
55
62
  end
56
63
  end
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ evaluator.divide(1.0, ::Math.cos(evaluator.evaluate_node(parameter_one)))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ evaluator.divide(1.0, ::Math.cosh(evaluator.evaluate_node(parameter_one)))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ ::Math.sin(evaluator.evaluate_node(parameter_one))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ ::Math.sinh(evaluator.evaluate_node(parameter_one))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -42,6 +42,10 @@ module Plurimath
42
42
  def to_unicodemath(options:)
43
43
  "√#{unicodemath_parens(parameter_one, options: options)}"
44
44
  end
45
+
46
+ def evaluate(evaluator)
47
+ ::Math.sqrt(evaluator.evaluate_node(parameter_one))
48
+ end
45
49
  end
46
50
  end
47
51
  end
@@ -13,6 +13,16 @@ module Plurimath
13
13
  third_value: "term",
14
14
  }.freeze
15
15
 
16
+ def evaluate(evaluator)
17
+ if parameter_one.nil? || parameter_two.nil? || parameter_three.nil?
18
+ evaluator.unsupported(self)
19
+ end
20
+
21
+ Evaluation::Iteration
22
+ .new(evaluator, parameter_one, parameter_two, parameter_three)
23
+ .accumulate(0, :+)
24
+ end
25
+
16
26
  def initialize(parameter_one = nil,
17
27
  parameter_two = nil,
18
28
  parameter_three = nil,
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ ::Math.tan(evaluator.evaluate_node(parameter_one))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Plurimath
7
7
  def validate_function_formula
8
8
  false
9
9
  end
10
+
11
+ def evaluate(evaluator)
12
+ ::Math.tanh(evaluator.evaluate_node(parameter_one))
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -66,6 +66,23 @@ module Plurimath
66
66
  false
67
67
  end
68
68
 
69
+ # OMML emits variable text runs as Text nodes. Only a plain, non-blank
70
+ # string names a variable; rich content (arrays/formulas) or a missing
71
+ # value does not.
72
+ def variable_name
73
+ return unless parameter_one.is_a?(String)
74
+
75
+ stripped = parameter_one.strip
76
+ stripped unless stripped.empty?
77
+ end
78
+
79
+ def evaluate(evaluator)
80
+ name = variable_name
81
+ evaluator.unsupported(self) unless name
82
+
83
+ evaluator.value_for(name)
84
+ end
85
+
69
86
  def to_asciimath_math_zone(spacing, _, _, options:)
70
87
  "#{spacing}#{to_asciimath(options: options)} text\n"
71
88
  end
@@ -3,48 +3,59 @@
3
3
  module Plurimath
4
4
  module Math
5
5
  class Number < Core
6
- attr_accessor :value, :mini_sub_sized, :mini_sup_sized
6
+ attr_accessor :value, :mini_sub_sized, :mini_sup_sized, :base
7
7
 
8
- def initialize(value = nil, mini_sub_sized: false, mini_sup_sized: false)
8
+ def initialize(value = nil, mini_sub_sized: false, mini_sup_sized: false,
9
+ base: nil)
9
10
  @value = value.is_a?(::Parslet::Slice) ? value.to_s : value
10
- @mini_sub_sized = mini_sub_sized if mini_sub_sized
11
- @mini_sup_sized = mini_sup_sized if mini_sup_sized
11
+ @mini_sub_sized = mini_sub_sized
12
+ @mini_sup_sized = mini_sup_sized
13
+ @base = base
12
14
  end
13
15
 
14
16
  def ==(object)
15
- object.respond_to?(:value) &&
17
+ object.is_a?(Number) &&
16
18
  object.value == value &&
17
19
  object.mini_sub_sized == mini_sub_sized &&
18
- object.mini_sup_sized == mini_sup_sized
20
+ object.mini_sup_sized == mini_sup_sized &&
21
+ object.base == base
19
22
  end
20
23
 
21
24
  def element_order=(*); end
22
25
 
23
26
  def to_asciimath(options:)
24
- format_value_with_options(options)
27
+ Formatter::Numbers::TextRenderer.render(
28
+ format_value_with_options(options), :asciimath
29
+ )
25
30
  end
26
31
 
27
32
  def to_mathml_without_math_tag(_, options:)
28
- Utility.ox_element("mn") << format_value_with_options(options)
33
+ Formatter::Numbers::MathmlRenderer.render(format_value_with_options(options))
29
34
  end
30
35
 
31
36
  def to_latex(options:)
32
- format_value_with_options(options)
37
+ Formatter::Numbers::TextRenderer.render(
38
+ format_value_with_options(options), :latex
39
+ )
33
40
  end
34
41
 
35
42
  def to_html(options:)
36
- format_value_with_options(options)
43
+ Formatter::Numbers::TextRenderer.render(
44
+ format_value_with_options(options), :html
45
+ )
37
46
  end
38
47
 
39
48
  def to_omml_without_math_tag(_, options:)
40
- [t_tag(options: options)]
49
+ [Formatter::Numbers::OmmlRenderer.render(format_value_with_options(options))]
41
50
  end
42
51
 
43
52
  def to_unicodemath(options:)
44
53
  return mini_sub if mini_sub_sized
45
54
  return mini_sup if mini_sup_sized
46
55
 
47
- format_value_with_options(options)
56
+ Formatter::Numbers::TextRenderer.render(
57
+ format_value_with_options(options), :unicodemath
58
+ )
48
59
  end
49
60
 
50
61
  def insert_t_tag(_, options:)
@@ -59,17 +70,26 @@ module Plurimath
59
70
 
60
71
  def t_tag(options:)
61
72
  Utility.ox_element("t",
62
- namespace: "m") << format_value_with_options(options)
73
+ namespace: "m") << formatted_value(options)
63
74
  end
64
75
 
65
76
  def nary_attr_value(options:)
66
- format_value_with_options(options)
77
+ format_value_with_options(options).to_s
67
78
  end
68
79
 
69
80
  def validate_function_formula
70
81
  false
71
82
  end
72
83
 
84
+ def evaluate(_evaluator)
85
+ raw_value = value.to_s
86
+ return raw_value.to_i if raw_value.match?(/\A[+-]?\d+\z/)
87
+
88
+ Float(raw_value)
89
+ rescue ArgumentError
90
+ raise Errors::Evaluation::UnsupportedExpressionError, "number `#{raw_value}`"
91
+ end
92
+
73
93
  def mini_sized?
74
94
  mini_sub_sized || mini_sup_sized
75
95
  end
@@ -98,25 +118,16 @@ module Plurimath
98
118
  end
99
119
  return value unless formatter
100
120
 
101
- if formatter.respond_to?(:format_number)
102
- formatter.format_number(options[:formula], self)
103
- elsif formatter.respond_to?(:format)
104
- formatter.format(options[:formula], self)
105
- elsif formatter.respond_to?(:localized_number)
106
- formatter.localized_number(value.to_s,
107
- **format_kwargs(options[:format]))
108
- else
109
- value
110
- end
121
+ format = options[:format] || {}
122
+ format = format.merge(base: base) if base && !format.key?(:base)
123
+
124
+ formatter.format_number(options[:formula], self, format: format)
111
125
  end
112
126
 
113
127
  private
114
128
 
115
- # Empty splat is silently dropped by Ruby, so simple formatter signatures still work.
116
- def format_kwargs(format)
117
- return {} unless format.is_a?(Hash) && !format.empty?
118
-
119
- { format: format }
129
+ def formatted_value(options)
130
+ format_value_with_options(options).to_s
120
131
  end
121
132
  end
122
133
  end
@@ -12,6 +12,10 @@ module Plurimath
12
12
  html: ["&#x22c5;", "&sdot;"],
13
13
  }.freeze
14
14
 
15
+ def multiply_operator?
16
+ true
17
+ end
18
+
15
19
  # output methods
16
20
  def to_latex(**)
17
21
  "\\cdot"
@@ -12,6 +12,10 @@ module Plurimath
12
12
  html: ["&#xf7;"],
13
13
  }.freeze
14
14
 
15
+ def divide_operator?
16
+ true
17
+ end
18
+
15
19
  # output methods
16
20
  def to_latex(**)
17
21
  "\\div"
@@ -11,6 +11,10 @@ module Plurimath
11
11
  html: ["&#x302;", "^"],
12
12
  }.freeze
13
13
 
14
+ def power_operator?
15
+ true
16
+ end
17
+
14
18
  # output methods
15
19
  def to_latex(**)
16
20
  "^"
@@ -13,6 +13,10 @@ module Plurimath
13
13
  html: ["&#x2212;", "-"],
14
14
  }.freeze
15
15
 
16
+ def minus_operator?
17
+ true
18
+ end
19
+
16
20
  # output methods
17
21
  def to_latex(**)
18
22
  "-"
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x3c0;", "&#x1d70b;"],
11
11
  latex: ["pi", "uppi", "&#x3c0;", "&#x1d70b;"],
12
12
  omml: ["&#x3c0;", "&#x1d70b;"],
13
- html: ["&#x3C0;", "&#x3c0;", "&#x1d70b;"],
13
+ html: ["&#x3C0;", "&#x3c0;", "&#x1d70b;", "π", "𝜋"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -37,6 +37,10 @@ module Plurimath
37
37
  def to_html(**)
38
38
  "&#x3C0;"
39
39
  end
40
+
41
+ def reserved_constant
42
+ ::Math::PI
43
+ end
40
44
  end
41
45
  end
42
46
  end
@@ -13,6 +13,10 @@ module Plurimath
13
13
  html: ["&#x2b;", "+"],
14
14
  }.freeze
15
15
 
16
+ def plus_operator?
17
+ true
18
+ end
19
+
16
20
  # output methods
17
21
  def to_latex(**)
18
22
  "+"
@@ -13,6 +13,10 @@ module Plurimath
13
13
  html: ["&#x2215;", "/"],
14
14
  }.freeze
15
15
 
16
+ def divide_operator?
17
+ true
18
+ end
19
+
16
20
  # output methods
17
21
  def to_latex(**)
18
22
  "/"
@@ -108,6 +108,51 @@ module Plurimath
108
108
  false
109
109
  end
110
110
 
111
+ # Some parser paths emit basic operators as generic symbols instead
112
+ # of their semantic classes, and a hand-built Formula may hold any of
113
+ # them, so the generic symbol answers for every operator from its own
114
+ # value (parsed `+`/`^` arrive as Plus/Hat, so these are inert there).
115
+ def plus_operator?
116
+ value == "+"
117
+ end
118
+
119
+ def minus_operator?
120
+ value == "-"
121
+ end
122
+
123
+ def multiply_operator?
124
+ value == "*"
125
+ end
126
+
127
+ def divide_operator?
128
+ value == "/"
129
+ end
130
+
131
+ def power_operator?
132
+ value == "^"
133
+ end
134
+
135
+ # A plain symbol names a variable through its `value`. Symbol
136
+ # subclasses (Pi, Plus, ...) carry no `value`, so they return nil here
137
+ # and are never treated as variables.
138
+ def variable_name
139
+ return if reserved_constant || value.nil? || value.empty? || operator?
140
+
141
+ value
142
+ end
143
+
144
+ # Symbol classes that represent constants (e.g. Pi) override
145
+ # #reserved_constant; everything else resolves through bindings.
146
+ def evaluate(evaluator)
147
+ constant = reserved_constant
148
+ return constant if constant
149
+
150
+ name = variable_name
151
+ evaluator.unsupported(self) unless name
152
+
153
+ evaluator.value_for(name)
154
+ end
155
+
111
156
  def omml_nodes(_, options:)
112
157
  Array(t_tag(options: options))
113
158
  end
@@ -13,6 +13,10 @@ module Plurimath
13
13
  html: ["&#xd7;", "&times;"],
14
14
  }.freeze
15
15
 
16
+ def multiply_operator?
17
+ true
18
+ end
19
+
16
20
  # output methods
17
21
  def to_latex(**)
18
22
  "\\times"
@@ -3,6 +3,7 @@
3
3
  module Plurimath
4
4
  module Math
5
5
  autoload :Core, "#{__dir__}/math/core"
6
+ autoload :Evaluation, "#{__dir__}/math/evaluation"
6
7
  autoload :Formula, "#{__dir__}/math/formula"
7
8
  autoload :Function, "#{__dir__}/math/function"
8
9
  autoload :InvalidTypeError, "#{__dir__}/errors/invalid_type_error"
@@ -174,6 +174,24 @@ module Plurimath
174
174
  "+": "+",
175
175
  "-": "-",
176
176
  }.freeze
177
+ # Over/under accent decorations. In MathML these are only ever expressed
178
+ # as <mover>/<munder> with a base (recovered from their diacritic
179
+ # CHARACTER), never as a bare <mi>/<mo> identifier word. So a bare token
180
+ # whose text is one of these names is a literal identifier (e.g. the unit
181
+ # "bar"), not the accent. See unitsml/unitsdb#123.
182
+ ACCENT_WORDS = %w[
183
+ bar overline ul hat vec tilde dot ddot obrace ubrace overleftrightarrow
184
+ ].freeze
185
+
186
+ # True when +string+ is a bare word naming an over/under accent. Such
187
+ # tokens are literal identifiers in MathML (the accent itself is a
188
+ # diacritic character inside <mover>/<munder>), so callers must not
189
+ # promote them to accent operators. AsciiMath/LaTeX word forms
190
+ # (bar(x), \bar{x}) are parsed elsewhere and never reach this predicate.
191
+ def self.accent_word?(string)
192
+ ACCENT_WORDS.include?(string&.strip)
193
+ end
194
+
177
195
  CLASSES = %w[
178
196
  mathfrak
179
197
  underset