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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Structured result of notation formatting (e, scientific, engineering).
|
|
7
|
+
# Carries the coefficient as a FormattedNumber, the exponent as an
|
|
8
|
+
# integer, and notation style metadata so output renderers (MathML,
|
|
9
|
+
# LaTeX, etc.) can produce structured representations instead of flat
|
|
10
|
+
# strings.
|
|
11
|
+
class FormattedNotation
|
|
12
|
+
STYLES = %i[e scientific engineering].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :coefficient, :notation_style, :exponent,
|
|
15
|
+
:times_symbol, :exponent_separator, :exponent_sign
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
coefficient:,
|
|
19
|
+
notation_style:,
|
|
20
|
+
exponent:,
|
|
21
|
+
times_symbol: "×",
|
|
22
|
+
exponent_separator: "e",
|
|
23
|
+
exponent_sign: nil
|
|
24
|
+
)
|
|
25
|
+
@coefficient = coefficient
|
|
26
|
+
@notation_style = notation_style
|
|
27
|
+
@exponent = exponent
|
|
28
|
+
@times_symbol = times_symbol
|
|
29
|
+
@exponent_separator = exponent_separator
|
|
30
|
+
@exponent_sign = exponent_sign
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
case notation_style
|
|
35
|
+
when :e
|
|
36
|
+
"#{coefficient}#{exponent_separator}#{formatted_exponent}"
|
|
37
|
+
when :scientific, :engineering
|
|
38
|
+
"#{coefficient} #{times_symbol} 10^#{formatted_exponent}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_str
|
|
43
|
+
to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def base_notation?
|
|
47
|
+
coefficient.base_notation?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The exponent rendered as a string, applying sign conventions.
|
|
51
|
+
# Public so that output renderers can access it directly.
|
|
52
|
+
def formatted_exponent
|
|
53
|
+
return "0" if exponent.zero?
|
|
54
|
+
|
|
55
|
+
sign_prefix = exponent_sign == :plus ? "+" : nil
|
|
56
|
+
abs_exp = exponent.abs.to_s
|
|
57
|
+
exponent.negative? ? "-#{abs_exp}" : "#{sign_prefix}#{abs_exp}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Structured result of number formatting. Carries sign, digit parts,
|
|
7
|
+
# decimal separator, and base notation as separate semantic elements
|
|
8
|
+
# so output renderers (MathML, LaTeX, etc.) can produce structured
|
|
9
|
+
# representations instead of flat strings.
|
|
10
|
+
class FormattedNumber
|
|
11
|
+
attr_reader :sign, :integer_part, :fraction_part,
|
|
12
|
+
:decimal_separator, :base_notation, :number_sign
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
sign:,
|
|
16
|
+
integer_part:,
|
|
17
|
+
fraction_part:,
|
|
18
|
+
decimal_separator:,
|
|
19
|
+
base_notation:,
|
|
20
|
+
number_sign: nil
|
|
21
|
+
)
|
|
22
|
+
@sign = sign
|
|
23
|
+
@integer_part = integer_part
|
|
24
|
+
@fraction_part = fraction_part
|
|
25
|
+
@decimal_separator = decimal_separator
|
|
26
|
+
@base_notation = base_notation
|
|
27
|
+
@number_sign = number_sign
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negative?
|
|
31
|
+
sign == -1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fractional?
|
|
35
|
+
!fraction_part.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_notation?
|
|
39
|
+
!base_notation.default?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The sign as a rendering prefix: "-" for negative, "+" when
|
|
43
|
+
# number_sign is :plus, nil otherwise. Output renderers use this
|
|
44
|
+
# to produce format-specific sign elements.
|
|
45
|
+
def sign_text
|
|
46
|
+
if negative?
|
|
47
|
+
"-"
|
|
48
|
+
elsif number_sign == :plus
|
|
49
|
+
"+"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
digits = formatted_digits
|
|
55
|
+
digits = base_notation.wrap(digits) unless base_notation.default?
|
|
56
|
+
"#{sign_text}#{digits}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_str
|
|
60
|
+
to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Digits with decimal separator and optional hex capitalization,
|
|
64
|
+
# but without sign, prefix, or postfix. Used by structured renderers
|
|
65
|
+
# that handle sign and base notation as separate elements.
|
|
66
|
+
def digits_string
|
|
67
|
+
formatted_digits
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def formatted_digits
|
|
73
|
+
digits = assembled_digits
|
|
74
|
+
base_notation.upcase_hex? ? upcase_hex(digits) : digits
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def assembled_digits
|
|
78
|
+
fractional? ? "#{integer_part}#{decimal_separator}#{fraction_part}" : integer_part
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def upcase_hex(string)
|
|
82
|
+
string.tr(Base::HEX_DIGITS, Base::HEX_DIGITS.upcase)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -20,7 +20,7 @@ module Plurimath
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Keep fraction preparation on structured parts; localized rendering and
|
|
23
|
-
# grouping happen later at the
|
|
23
|
+
# grouping happen later at the FormattedNumber boundary.
|
|
24
24
|
def apply_parts(parts, precision: self.precision)
|
|
25
25
|
precision = precision.to_i
|
|
26
26
|
return parts.with_digits(fraction_digits: DEFAULT_STRINGS[:empty]) unless precision.positive?
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Renders a Formatter result (FormattedNotation, FormattedNumber, or
|
|
7
|
+
# String) into a MathML element tree. Number delegates to this so the
|
|
8
|
+
# model does not carry format-specific XML construction.
|
|
9
|
+
module MathmlRenderer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def render(result)
|
|
13
|
+
case result
|
|
14
|
+
when FormattedNotation then render_notation(result)
|
|
15
|
+
when FormattedNumber
|
|
16
|
+
result.base_notation.semantic? ? render_semantic_base(result) : plain_element(result)
|
|
17
|
+
else
|
|
18
|
+
plain_element(result)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_notation(notation)
|
|
23
|
+
return plain_element(notation) if notation.notation_style == :e
|
|
24
|
+
|
|
25
|
+
coeff = plain_element(notation.coefficient)
|
|
26
|
+
times = Utility.ox_element("mo") << notation.times_symbol.to_s
|
|
27
|
+
base_el = Utility.ox_element("mn") << "10"
|
|
28
|
+
exp_el = Utility.ox_element("mn") << notation.formatted_exponent
|
|
29
|
+
sup = Utility.ox_element("msup")
|
|
30
|
+
Utility.update_nodes(sup, [base_el, exp_el])
|
|
31
|
+
row = Utility.ox_element("mrow")
|
|
32
|
+
Utility.update_nodes(row, [coeff, times, sup])
|
|
33
|
+
row
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_semantic_base(formatted)
|
|
37
|
+
digits = Utility.ox_element("mn") << formatted.digits_string
|
|
38
|
+
base_el = Utility.ox_element("mn") << formatted.base_notation.base.to_s
|
|
39
|
+
sub = Utility.ox_element("msub")
|
|
40
|
+
Utility.update_nodes(sub, [digits, base_el])
|
|
41
|
+
|
|
42
|
+
return sub unless formatted.sign_text
|
|
43
|
+
|
|
44
|
+
sign_el = Utility.ox_element("mo") << formatted.sign_text
|
|
45
|
+
row = Utility.ox_element("mrow")
|
|
46
|
+
Utility.update_nodes(row, [sign_el, sub])
|
|
47
|
+
row
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def plain_element(result)
|
|
51
|
+
Utility.ox_element("mn") << result.to_s
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -11,7 +11,6 @@ module Plurimath
|
|
|
11
11
|
def initialize(options)
|
|
12
12
|
@options = options
|
|
13
13
|
@precision = (@options.precision || 0).to_i
|
|
14
|
-
@exponent_sign_renderer = SignRenderer.new(@options.exponent_sign)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def render(source, notation)
|
|
@@ -31,25 +30,43 @@ module Plurimath
|
|
|
31
30
|
|
|
32
31
|
private
|
|
33
32
|
|
|
34
|
-
attr_reader :
|
|
33
|
+
attr_reader :options, :precision
|
|
35
34
|
|
|
36
35
|
def render_e(source)
|
|
37
|
-
|
|
36
|
+
coefficient, exponent = resolve_notation_parts(source,
|
|
37
|
+
precision: precision)
|
|
38
|
+
build_notation(coefficient, :e, exponent)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def render_scientific(source)
|
|
41
|
-
|
|
42
|
+
coefficient, exponent = resolve_notation_parts(source,
|
|
43
|
+
precision: precision)
|
|
44
|
+
build_notation(coefficient, :scientific, exponent)
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
def render_engineering(source)
|
|
45
48
|
parts = notation_parts(source)
|
|
46
|
-
parts =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
parts = engineering_coefficient_parts(parts) unless source.decimal.zero?
|
|
50
|
+
coefficient = localize_parts(source, parts[0],
|
|
51
|
+
precision: engineering_precision(source, parts[0]))
|
|
52
|
+
build_notation(coefficient, :engineering, parts[1])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_notation_parts(source, precision:)
|
|
56
|
+
parts = notation_parts(source)
|
|
57
|
+
coefficient = localize_parts(source, parts[0], precision: precision)
|
|
58
|
+
[coefficient, parts[1]]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_notation(coefficient, style, exponent)
|
|
62
|
+
FormattedNotation.new(
|
|
63
|
+
coefficient: coefficient,
|
|
64
|
+
notation_style: style,
|
|
65
|
+
exponent: exponent,
|
|
66
|
+
times_symbol: options.times,
|
|
67
|
+
exponent_separator: options.exponent_separator.to_s,
|
|
68
|
+
exponent_sign: options.exponent_sign,
|
|
51
69
|
)
|
|
52
|
-
parts.join(" #{options.times} 10^")
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
def localize_parts(source, parts, precision:)
|
|
@@ -62,13 +79,6 @@ module Plurimath
|
|
|
62
79
|
)
|
|
63
80
|
end
|
|
64
81
|
|
|
65
|
-
def localized_notation_parts(source)
|
|
66
|
-
parts = notation_parts(source)
|
|
67
|
-
parts[0] = localize_parts(source, parts[0], precision: precision)
|
|
68
|
-
parts[1] = render_exponent(parts[1])
|
|
69
|
-
parts
|
|
70
|
-
end
|
|
71
|
-
|
|
72
82
|
def notation_parts(source)
|
|
73
83
|
if source.decimal.zero?
|
|
74
84
|
return [source.to_parts(base: options.base),
|
|
@@ -88,10 +98,10 @@ module Plurimath
|
|
|
88
98
|
]
|
|
89
99
|
end
|
|
90
100
|
|
|
91
|
-
def
|
|
101
|
+
def engineering_coefficient_parts(parts)
|
|
92
102
|
coefficient, exponent = parts
|
|
93
103
|
index = exponent % 3
|
|
94
|
-
exponent
|
|
104
|
+
new_exponent = exponent - index
|
|
95
105
|
digits = "#{coefficient.integer_digits}#{coefficient.fraction_digits}"
|
|
96
106
|
integer_length = index + 1
|
|
97
107
|
integer_digits = digits[0...integer_length].to_s.ljust(
|
|
@@ -105,7 +115,7 @@ module Plurimath
|
|
|
105
115
|
integer_digits: integer_digits,
|
|
106
116
|
fraction_digits: digits[integer_length..].to_s,
|
|
107
117
|
),
|
|
108
|
-
|
|
118
|
+
new_exponent,
|
|
109
119
|
]
|
|
110
120
|
end
|
|
111
121
|
|
|
@@ -115,10 +125,7 @@ module Plurimath
|
|
|
115
125
|
# shifted integer width. Only a positive explicit precision is taken
|
|
116
126
|
# as a literal fraction width; precision: 0 falls through to inference.
|
|
117
127
|
def engineering_precision(source, coefficient)
|
|
118
|
-
# precision: 0 is intentionally not literal here; it infers.
|
|
119
128
|
return precision if options.explicit_precision? && precision.positive?
|
|
120
|
-
# Zero sources carry their stated fraction width; an explicit
|
|
121
|
-
# precision: 0 still infers (consistent with non-zero engineering).
|
|
122
129
|
return source.notation_precision if source.decimal.zero?
|
|
123
130
|
|
|
124
131
|
integer_length = coefficient.integer_digits.length
|
|
@@ -145,12 +152,6 @@ module Plurimath
|
|
|
145
152
|
[digits[index..], parts.integer_digits.length - index - 1]
|
|
146
153
|
end
|
|
147
154
|
end
|
|
148
|
-
|
|
149
|
-
def render_exponent(exponent)
|
|
150
|
-
return "0" if exponent.zero?
|
|
151
|
-
|
|
152
|
-
exponent_sign_renderer.apply(exponent, exponent.abs.to_s)
|
|
153
|
-
end
|
|
154
155
|
end
|
|
155
156
|
end
|
|
156
157
|
end
|
|
@@ -11,15 +11,10 @@ module Plurimath
|
|
|
11
11
|
def initialize(source, options)
|
|
12
12
|
@source = source
|
|
13
13
|
@options = options
|
|
14
|
-
@base_notation = BaseNotation.
|
|
14
|
+
@base_notation = BaseNotation.from_options(@options)
|
|
15
15
|
@integer_format = Integer.new(@options)
|
|
16
16
|
@fraction_format = Fraction.new(@options)
|
|
17
17
|
@significant_format = Significant.new(@options)
|
|
18
|
-
@parts_renderer = PartsRenderer.new(
|
|
19
|
-
integer_formatter: @integer_format,
|
|
20
|
-
fraction_formatter: @fraction_format,
|
|
21
|
-
)
|
|
22
|
-
@sign_renderer = SignRenderer.new(@options.number_sign)
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
def format(precision: nil)
|
|
@@ -41,15 +36,21 @@ module Plurimath
|
|
|
41
36
|
parts = renderable_parts(parts, precision: precision)
|
|
42
37
|
|
|
43
38
|
parts = significant_format.apply_parts(parts) if significant_format.active?
|
|
44
|
-
result = parts_renderer.render(parts)
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
FormattedNumber.new(
|
|
41
|
+
sign: parts.sign,
|
|
42
|
+
integer_part: integer_format.format_groups(parts.integer_digits),
|
|
43
|
+
fraction_part: parts.fractional? ? fraction_format.format_groups(parts.fraction_digits) : "",
|
|
44
|
+
decimal_separator: fraction_format.decimal,
|
|
45
|
+
base_notation: base_notation,
|
|
46
|
+
number_sign: options.number_sign,
|
|
47
|
+
)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
51
52
|
attr_reader :base_notation, :fraction_format, :integer_format,
|
|
52
|
-
:
|
|
53
|
+
:significant_format
|
|
53
54
|
|
|
54
55
|
def renderable_parts(parts, precision:)
|
|
55
56
|
parts = parts.with_digits(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Renders a Formatter result (FormattedNotation, FormattedNumber, or
|
|
7
|
+
# String) into an OMML element tree. Number delegates to this so the
|
|
8
|
+
# model does not carry format-specific XML construction.
|
|
9
|
+
module OmmlRenderer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def render(result)
|
|
13
|
+
case result
|
|
14
|
+
when FormattedNotation then render_notation(result)
|
|
15
|
+
when FormattedNumber
|
|
16
|
+
result.base_notation.semantic? ? render_semantic_base(result) : plain_element(result)
|
|
17
|
+
else
|
|
18
|
+
plain_element(result)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_notation(notation)
|
|
23
|
+
return plain_element(notation) if notation.notation_style == :e
|
|
24
|
+
|
|
25
|
+
sup_struct = Utility.ox_element("sSup", namespace: "m")
|
|
26
|
+
subpr = Utility.ox_element("sSupPr", namespace: "m")
|
|
27
|
+
subpr << Utility.pr_element("ctrl", true, namespace: "m")
|
|
28
|
+
|
|
29
|
+
coeff_run = text_run(notation.coefficient.to_s)
|
|
30
|
+
times_run = text_run(" #{notation.times_symbol} ")
|
|
31
|
+
base_run = text_run("10")
|
|
32
|
+
exp_run = text_run(notation.formatted_exponent)
|
|
33
|
+
|
|
34
|
+
e_el = Utility.ox_element("e", namespace: "m")
|
|
35
|
+
Utility.update_nodes(e_el, [coeff_run, times_run, base_run])
|
|
36
|
+
|
|
37
|
+
sup_el = Utility.ox_element("sup", namespace: "m")
|
|
38
|
+
Utility.update_nodes(sup_el, [exp_run])
|
|
39
|
+
|
|
40
|
+
Utility.update_nodes(sup_struct, [subpr, e_el, sup_el])
|
|
41
|
+
sup_struct
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_semantic_base(formatted)
|
|
45
|
+
sub_struct = Utility.ox_element("sSub", namespace: "m")
|
|
46
|
+
subpr = Utility.ox_element("sSubPr", namespace: "m")
|
|
47
|
+
subpr << Utility.pr_element("ctrl", true, namespace: "m")
|
|
48
|
+
|
|
49
|
+
digits_with_sign = "#{formatted.sign_text}#{formatted.digits_string}"
|
|
50
|
+
base_run = text_run(digits_with_sign)
|
|
51
|
+
sub_run = text_run(formatted.base_notation.base.to_s)
|
|
52
|
+
|
|
53
|
+
e_el = Utility.ox_element("e", namespace: "m")
|
|
54
|
+
Utility.update_nodes(e_el, [base_run])
|
|
55
|
+
|
|
56
|
+
sub_el = Utility.ox_element("sub", namespace: "m")
|
|
57
|
+
Utility.update_nodes(sub_el, [sub_run])
|
|
58
|
+
|
|
59
|
+
Utility.update_nodes(sub_struct, [subpr, e_el, sub_el])
|
|
60
|
+
sub_struct
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def plain_element(result)
|
|
64
|
+
Utility.ox_element("t", namespace: "m") << result.to_s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def text_run(text)
|
|
68
|
+
run = Utility.ox_element("r", namespace: "m")
|
|
69
|
+
run << (Utility.ox_element("t", namespace: "m") << text)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -17,10 +17,11 @@ module Plurimath
|
|
|
17
17
|
# junk after partial parses, and Infinity/NaN spellings.
|
|
18
18
|
NUMERIC_PATTERN = /\A[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?\z/
|
|
19
19
|
|
|
20
|
-
def initialize(value)
|
|
20
|
+
def initialize(value, base: Base::DEFAULT_BASE)
|
|
21
21
|
@raw = value.to_s
|
|
22
|
+
@base = base || Base::DEFAULT_BASE
|
|
22
23
|
validate_numeric!(value)
|
|
23
|
-
@decimal =
|
|
24
|
+
@decimal = parse_decimal
|
|
24
25
|
@sign = raw.start_with?("-") ? -1 : 1
|
|
25
26
|
|
|
26
27
|
mantissa, @exponent_text = unsigned_value.split("e", 2)
|
|
@@ -79,13 +80,27 @@ module Plurimath
|
|
|
79
80
|
|
|
80
81
|
private
|
|
81
82
|
|
|
83
|
+
# Converts the raw string to BigDecimal. For non-decimal bases with
|
|
84
|
+
# alphanumeric digits (e.g. "FF" in hex), uses #to_i(base) to convert.
|
|
85
|
+
# Pure-decimal inputs (e.g. "255") are parsed directly by BigDecimal.
|
|
86
|
+
def parse_decimal
|
|
87
|
+
return BigDecimal(raw) if decimal_input?
|
|
88
|
+
|
|
89
|
+
stripped = raw.sub(/\A[-+]/, "")
|
|
90
|
+
BigDecimal(stripped.to_i(@base))
|
|
91
|
+
end
|
|
92
|
+
|
|
82
93
|
def validate_numeric!(value)
|
|
83
94
|
valid_type = value.is_a?(Numeric) || value.is_a?(String)
|
|
84
|
-
return if valid_type && NUMERIC_PATTERN.match?(raw)
|
|
95
|
+
return if valid_type && (non_decimal_base? || NUMERIC_PATTERN.match?(raw))
|
|
85
96
|
|
|
86
97
|
raise Plurimath::Errors::InvalidNumber, value
|
|
87
98
|
end
|
|
88
99
|
|
|
100
|
+
def non_decimal_base?
|
|
101
|
+
@base != Base::DEFAULT_BASE
|
|
102
|
+
end
|
|
103
|
+
|
|
89
104
|
def decimal_parts_integer_length
|
|
90
105
|
parts = to_parts
|
|
91
106
|
return 0 if parts.integer_zero?
|
|
@@ -127,7 +142,17 @@ module Plurimath
|
|
|
127
142
|
end
|
|
128
143
|
|
|
129
144
|
def unsigned_value
|
|
130
|
-
raw.downcase.sub(/\A[-+]/, "")
|
|
145
|
+
return raw.downcase.sub(/\A[-+]/, "") if decimal_input?
|
|
146
|
+
|
|
147
|
+
@decimal.abs.to_s("F").downcase.sub(/\A[-+]/, "").delete_suffix(".0")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns true when the raw value is a plain decimal string (digits,
|
|
151
|
+
# optional decimal point, optional exponent), meaning BigDecimal(raw)
|
|
152
|
+
# works directly.
|
|
153
|
+
def decimal_input?
|
|
154
|
+
stripped = raw.sub(/\A[-+]/, "")
|
|
155
|
+
stripped.match?(/\A[0-9.]+(?:e[+-]?[0-9]+)?\z/i)
|
|
131
156
|
end
|
|
132
157
|
end
|
|
133
158
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Renders a Formatter result (FormattedNotation, FormattedNumber, or
|
|
7
|
+
# String) into a plain-text format (LaTeX, AsciiMath, HTML, UnicodeMath).
|
|
8
|
+
# For semantic base notation, each format's template lives in
|
|
9
|
+
# BASE_TEMPLATES; adding a new text format is a one-line change there.
|
|
10
|
+
# When the caller supplies an explicit base_prefix/base_postfix, the
|
|
11
|
+
# literal prefix+postfix take precedence (no format-specific decoration).
|
|
12
|
+
module TextRenderer
|
|
13
|
+
# %<sign>s %<digits>s %<base>d — sign_text returns "" for positives
|
|
14
|
+
# without explicit :plus, so interpolation collapses cleanly.
|
|
15
|
+
BASE_TEMPLATES = {
|
|
16
|
+
asciimath: "%<sign>s%<digits>s_(%<base>d)",
|
|
17
|
+
unicodemath: "%<sign>s%<digits>s_(%<base>d)",
|
|
18
|
+
latex: "%<sign>s\\mathrm{%<digits>s}_{%<base>d}",
|
|
19
|
+
html: "%<sign>s%<digits>s<sub>%<base>d</sub>",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def render(result, format)
|
|
25
|
+
return result.to_s unless structured_number?(result)
|
|
26
|
+
|
|
27
|
+
if result.base_notation.literal?
|
|
28
|
+
result.to_s
|
|
29
|
+
else
|
|
30
|
+
render_semantic(result, format)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def structured_number?(result)
|
|
35
|
+
result.is_a?(FormattedNumber) && result.base_notation?
|
|
36
|
+
end
|
|
37
|
+
private_class_method :structured_number?
|
|
38
|
+
|
|
39
|
+
def render_semantic(result, format)
|
|
40
|
+
return result.to_s unless result.base_notation.semantic?
|
|
41
|
+
|
|
42
|
+
template = BASE_TEMPLATES.fetch(format)
|
|
43
|
+
format(template,
|
|
44
|
+
sign: result.sign_text.to_s,
|
|
45
|
+
digits: result.digits_string,
|
|
46
|
+
base: result.base_notation.base)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :render_semantic
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -4,20 +4,24 @@ module Plurimath
|
|
|
4
4
|
module Formatter
|
|
5
5
|
module Numbers
|
|
6
6
|
autoload :Base, "#{__dir__}/numbers/base"
|
|
7
|
-
autoload :DigitSequence, "#{__dir__}/numbers/digit_sequence"
|
|
8
7
|
autoload :BaseNotation, "#{__dir__}/numbers/base_notation"
|
|
8
|
+
autoload :DigitSequence, "#{__dir__}/numbers/digit_sequence"
|
|
9
9
|
autoload :Fraction, "#{__dir__}/numbers/fraction"
|
|
10
10
|
autoload :FormatOptions, "#{__dir__}/numbers/format_options"
|
|
11
|
+
autoload :FormattedNumber, "#{__dir__}/numbers/formatted_number"
|
|
12
|
+
autoload :FormattedNotation, "#{__dir__}/numbers/formatted_notation"
|
|
11
13
|
autoload :Integer, "#{__dir__}/numbers/integer"
|
|
14
|
+
autoload :MathmlRenderer, "#{__dir__}/numbers/mathml_renderer"
|
|
12
15
|
autoload :NumberRenderer, "#{__dir__}/numbers/number_renderer"
|
|
13
16
|
autoload :NotationRenderer, "#{__dir__}/numbers/notation_renderer"
|
|
17
|
+
autoload :OmmlRenderer, "#{__dir__}/numbers/omml_renderer"
|
|
14
18
|
autoload :Parts, "#{__dir__}/numbers/parts"
|
|
15
|
-
autoload :PartsRenderer, "#{__dir__}/numbers/parts_renderer"
|
|
16
19
|
autoload :PrecisionResolver, "#{__dir__}/numbers/precision_resolver"
|
|
17
20
|
autoload :SignRenderer, "#{__dir__}/numbers/sign_renderer"
|
|
18
21
|
autoload :Significant, "#{__dir__}/numbers/significant"
|
|
19
22
|
autoload :Source, "#{__dir__}/numbers/source"
|
|
20
23
|
autoload :SymbolResolver, "#{__dir__}/numbers/symbol_resolver"
|
|
24
|
+
autoload :TextRenderer, "#{__dir__}/numbers/text_renderer"
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
end
|
data/lib/plurimath/html/parse.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
class Html
|
|
5
5
|
class Parse < Parslet::Parser
|
|
6
|
+
include Plurimath::BaseNumberPrefix::Parser
|
|
7
|
+
|
|
6
8
|
rule(:space) { match["\s"].repeat(1) }
|
|
7
9
|
rule(:unary) { array_to_expression(Constants::UNARY_CLASSES, :unary) }
|
|
8
10
|
rule(:binary) { str("lim").as(:binary) }
|
|
@@ -75,6 +77,9 @@ module Plurimath
|
|
|
75
77
|
rule(:symbol_text_or_tag) do
|
|
76
78
|
tag_parse |
|
|
77
79
|
html_entity.as(:symbol) |
|
|
80
|
+
hex_number |
|
|
81
|
+
binary_number |
|
|
82
|
+
octal_number |
|
|
78
83
|
(match["0-9"].repeat(1) >> decimal_marker >> match["0-9"].repeat(1)).as(:number) |
|
|
79
84
|
match["0-9"].repeat(1).as(:number) |
|
|
80
85
|
match["a-zA-Z"].as(:text) |
|
|
@@ -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) }
|
|
@@ -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)) }
|