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
|
@@ -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,21 +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
|
+
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,
|
|
69
|
+
)
|
|
49
70
|
end
|
|
50
71
|
|
|
51
72
|
def localize_parts(source, parts, precision:)
|
|
@@ -58,15 +79,11 @@ module Plurimath
|
|
|
58
79
|
)
|
|
59
80
|
end
|
|
60
81
|
|
|
61
|
-
def localized_notation_parts(source)
|
|
62
|
-
parts = notation_parts(source)
|
|
63
|
-
parts[0] = localize_parts(source, parts[0], precision: precision)
|
|
64
|
-
parts[1] = render_exponent(parts[1])
|
|
65
|
-
parts
|
|
66
|
-
end
|
|
67
|
-
|
|
68
82
|
def notation_parts(source)
|
|
69
|
-
|
|
83
|
+
if source.decimal.zero?
|
|
84
|
+
return [source.to_parts(base: options.base),
|
|
85
|
+
0]
|
|
86
|
+
end
|
|
70
87
|
|
|
71
88
|
parts = source.to_parts(base: Base::DEFAULT_BASE)
|
|
72
89
|
digits, exponent = significant_digits_and_exponent(parts)
|
|
@@ -81,13 +98,15 @@ module Plurimath
|
|
|
81
98
|
]
|
|
82
99
|
end
|
|
83
100
|
|
|
84
|
-
def
|
|
101
|
+
def engineering_coefficient_parts(parts)
|
|
85
102
|
coefficient, exponent = parts
|
|
86
103
|
index = exponent % 3
|
|
87
|
-
exponent
|
|
104
|
+
new_exponent = exponent - index
|
|
88
105
|
digits = "#{coefficient.integer_digits}#{coefficient.fraction_digits}"
|
|
89
106
|
integer_length = index + 1
|
|
90
|
-
integer_digits = digits[0...integer_length].to_s.ljust(
|
|
107
|
+
integer_digits = digits[0...integer_length].to_s.ljust(
|
|
108
|
+
integer_length, "0"
|
|
109
|
+
)
|
|
91
110
|
|
|
92
111
|
[
|
|
93
112
|
Parts.new(
|
|
@@ -96,14 +115,31 @@ module Plurimath
|
|
|
96
115
|
integer_digits: integer_digits,
|
|
97
116
|
fraction_digits: digits[integer_length..].to_s,
|
|
98
117
|
),
|
|
99
|
-
|
|
118
|
+
new_exponent,
|
|
100
119
|
]
|
|
101
120
|
end
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
# The inferred budget covers the source's significant digits; the
|
|
123
|
+
# engineering shift moves one to three of them into the integer part
|
|
124
|
+
# (per exponent mod 3), so the fraction budget must subtract the
|
|
125
|
+
# shifted integer width. Only a positive explicit precision is taken
|
|
126
|
+
# as a literal fraction width; precision: 0 falls through to inference.
|
|
127
|
+
def engineering_precision(source, coefficient)
|
|
128
|
+
return precision if options.explicit_precision? && precision.positive?
|
|
129
|
+
return source.notation_precision if source.decimal.zero?
|
|
130
|
+
|
|
131
|
+
integer_length = coefficient.integer_digits.length
|
|
132
|
+
budget = [options.significant, options.digit_count].max
|
|
133
|
+
unless budget.positive?
|
|
134
|
+
return [source.significant_digit_count - integer_length,
|
|
135
|
+
0].max
|
|
136
|
+
end
|
|
105
137
|
|
|
106
|
-
[
|
|
138
|
+
fraction_budget = [budget - integer_length, 0].max
|
|
139
|
+
# Leave one digit for Significant's rounding pass when the source
|
|
140
|
+
# carries more digits than the requested budget.
|
|
141
|
+
fraction_budget += 1 if source.significant_digit_count > budget
|
|
142
|
+
fraction_budget
|
|
107
143
|
end
|
|
108
144
|
|
|
109
145
|
def significant_digits_and_exponent(parts)
|
|
@@ -116,12 +152,6 @@ module Plurimath
|
|
|
116
152
|
[digits[index..], parts.integer_digits.length - index - 1]
|
|
117
153
|
end
|
|
118
154
|
end
|
|
119
|
-
|
|
120
|
-
def render_exponent(exponent)
|
|
121
|
-
return "0" if exponent.zero?
|
|
122
|
-
|
|
123
|
-
exponent_sign_renderer.apply(exponent, exponent.abs.to_s)
|
|
124
|
-
end
|
|
125
155
|
end
|
|
126
156
|
end
|
|
127
157
|
end
|
|
@@ -11,42 +11,46 @@ 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)
|
|
26
21
|
render_precision = precision || options.precision || source_precision
|
|
27
22
|
parts = source.to_parts(
|
|
28
23
|
base: options.base,
|
|
29
|
-
precision: render_precision,
|
|
24
|
+
precision: decimal_precision_for(render_precision),
|
|
30
25
|
)
|
|
31
26
|
format_parts(parts, precision: render_precision)
|
|
32
27
|
end
|
|
33
28
|
|
|
34
29
|
def format_parts(parts, precision:)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
decimal_precision = decimal_precision_for(precision)
|
|
31
|
+
unless decimal_precision.nil?
|
|
32
|
+
parts = parts.with_digits(
|
|
33
|
+
fraction_digits: parts.fraction_digits[0...decimal_precision.to_i].to_s,
|
|
34
|
+
)
|
|
35
|
+
end
|
|
38
36
|
parts = renderable_parts(parts, precision: precision)
|
|
39
37
|
|
|
40
38
|
parts = significant_format.apply_parts(parts) if significant_format.active?
|
|
41
|
-
result = parts_renderer.render(parts)
|
|
42
39
|
|
|
43
|
-
|
|
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
|
+
)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
private
|
|
47
51
|
|
|
48
52
|
attr_reader :base_notation, :fraction_format, :integer_format,
|
|
49
|
-
:
|
|
53
|
+
:significant_format
|
|
50
54
|
|
|
51
55
|
def renderable_parts(parts, precision:)
|
|
52
56
|
parts = parts.with_digits(
|
|
@@ -60,6 +64,15 @@ module Plurimath
|
|
|
60
64
|
|
|
61
65
|
source.fraction_digits.length
|
|
62
66
|
end
|
|
67
|
+
|
|
68
|
+
# Precision budgets target-base digits, so the decimal fraction must
|
|
69
|
+
# stay intact until Fraction#change_base generates them; truncating
|
|
70
|
+
# decimal digits first loses value (0.07 in hex became 0x0.0).
|
|
71
|
+
def decimal_precision_for(precision)
|
|
72
|
+
return precision if options.base == Base::DEFAULT_BASE
|
|
73
|
+
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
63
76
|
end
|
|
64
77
|
end
|
|
65
78
|
end
|
|
@@ -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
|
|
@@ -5,17 +5,30 @@ module Plurimath
|
|
|
5
5
|
module Numbers
|
|
6
6
|
# Chooses which precision source wins for one render call.
|
|
7
7
|
class PrecisionResolver
|
|
8
|
-
def resolve(source, precision:, base:, significant:,
|
|
8
|
+
def resolve(source, precision:, base:, significant:,
|
|
9
|
+
notation_supported:, digit_count: 0)
|
|
9
10
|
return precision if precision
|
|
10
11
|
|
|
11
|
-
significant_precision = significant_base_precision(source, base,
|
|
12
|
+
significant_precision = significant_base_precision(source, base,
|
|
13
|
+
significant)
|
|
12
14
|
return significant_precision if significant_precision
|
|
13
15
|
|
|
14
16
|
# Source owns input-derived digit lengths; this resolver only decides
|
|
15
17
|
# which precision rule wins. Plain decimal rendering preserves
|
|
16
18
|
# fractional scale, while notation precision controls coefficient
|
|
17
19
|
# digits after the leading digit.
|
|
18
|
-
|
|
20
|
+
if notation_supported
|
|
21
|
+
# A requested digit budget (significant or digit_count) widens the
|
|
22
|
+
# coefficient fraction beyond the source's own significant digits
|
|
23
|
+
# (one digit leads, so the fraction allowance is budget - 1).
|
|
24
|
+
budget = [significant, digit_count].max
|
|
25
|
+
if budget.positive?
|
|
26
|
+
return [budget - 1,
|
|
27
|
+
source.notation_precision].max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return source.notation_precision
|
|
31
|
+
end
|
|
19
32
|
|
|
20
33
|
source.decimal_precision
|
|
21
34
|
end
|
|
@@ -75,7 +75,8 @@ module Plurimath
|
|
|
75
75
|
|
|
76
76
|
fraction = reversed[0...decimal_index]
|
|
77
77
|
integer = reversed[(decimal_index + 1)..]
|
|
78
|
-
fraction, carry = digit_sequence.increment_reversed(fraction,
|
|
78
|
+
fraction, carry = digit_sequence.increment_reversed(fraction,
|
|
79
|
+
overflow: EMPTY_STRING)
|
|
79
80
|
return fraction + [CANONICAL_DECIMAL] + integer unless carry.positive?
|
|
80
81
|
|
|
81
82
|
integer = increment_integer(integer)
|
|
@@ -83,7 +84,8 @@ module Plurimath
|
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def increment_integer(reversed)
|
|
86
|
-
digits, carry = digit_sequence.increment_reversed(reversed,
|
|
87
|
+
digits, carry = digit_sequence.increment_reversed(reversed,
|
|
88
|
+
overflow: ZERO)
|
|
87
89
|
digits << "1" if carry.positive?
|
|
88
90
|
digits
|
|
89
91
|
end
|
|
@@ -13,10 +13,15 @@ module Plurimath
|
|
|
13
13
|
|
|
14
14
|
DEFAULT_INTEGER = "0"
|
|
15
15
|
EMPTY_STRING = ""
|
|
16
|
+
# Stricter than BigDecimal(), which tolerates underscores, surrounding
|
|
17
|
+
# junk after partial parses, and Infinity/NaN spellings.
|
|
18
|
+
NUMERIC_PATTERN = /\A[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?\z/
|
|
16
19
|
|
|
17
|
-
def initialize(value)
|
|
20
|
+
def initialize(value, base: Base::DEFAULT_BASE)
|
|
18
21
|
@raw = value.to_s
|
|
19
|
-
@
|
|
22
|
+
@base = base || Base::DEFAULT_BASE
|
|
23
|
+
validate_numeric!(value)
|
|
24
|
+
@decimal = parse_decimal
|
|
20
25
|
@sign = raw.start_with?("-") ? -1 : 1
|
|
21
26
|
|
|
22
27
|
mantissa, @exponent_text = unsigned_value.split("e", 2)
|
|
@@ -33,9 +38,13 @@ module Plurimath
|
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def notation_precision
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
# A zero coefficient keeps the source's stated fraction width.
|
|
42
|
+
return fraction_digits.length if decimal.zero?
|
|
43
|
+
|
|
44
|
+
# The coefficient's fraction width is its significant-digit count
|
|
45
|
+
# minus the single leading digit; the sign and leading zeros carry
|
|
46
|
+
# no precision.
|
|
47
|
+
[significant_digit_count - 1, 0].max
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
def significant_digit_count
|
|
@@ -71,6 +80,27 @@ module Plurimath
|
|
|
71
80
|
|
|
72
81
|
private
|
|
73
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
|
+
|
|
93
|
+
def validate_numeric!(value)
|
|
94
|
+
valid_type = value.is_a?(Numeric) || value.is_a?(String)
|
|
95
|
+
return if valid_type && (non_decimal_base? || NUMERIC_PATTERN.match?(raw))
|
|
96
|
+
|
|
97
|
+
raise Plurimath::Errors::InvalidNumber, value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def non_decimal_base?
|
|
101
|
+
@base != Base::DEFAULT_BASE
|
|
102
|
+
end
|
|
103
|
+
|
|
74
104
|
def decimal_parts_integer_length
|
|
75
105
|
parts = to_parts
|
|
76
106
|
return 0 if parts.integer_zero?
|
|
@@ -112,7 +142,17 @@ module Plurimath
|
|
|
112
142
|
end
|
|
113
143
|
|
|
114
144
|
def unsigned_value
|
|
115
|
-
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)
|
|
116
156
|
end
|
|
117
157
|
end
|
|
118
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
|
|
@@ -35,6 +35,12 @@ module Plurimath
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def set_default_options(options)
|
|
38
|
+
unless options.nil? || options.is_a?(Hash)
|
|
39
|
+
raise Plurimath::ConfigurationError.new(
|
|
40
|
+
:invalid_formatter_option, option: :options, value: options, supported: "a Hash"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
options = options ? options.dup : {}
|
|
39
45
|
apply_default_symbols(options)
|
|
40
46
|
options
|
|
@@ -118,7 +118,7 @@ module Plurimath
|
|
|
118
118
|
locale_key = key_for(locale)
|
|
119
119
|
return locale_key if locale.nil? || locale_key
|
|
120
120
|
|
|
121
|
-
raise UnsupportedLocale.new(locale, LOCALES.keys)
|
|
121
|
+
raise Plurimath::Errors::UnsupportedLocale.new(locale, LOCALES.keys)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def key_for(locale)
|
data/lib/plurimath/formatter.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
autoload :SupportedLocales, "#{__dir__}/formatter/supported_locales"
|
|
6
|
-
autoload :UnsupportedBase, "#{__dir__}/errors/formatter/unsupported_base"
|
|
7
|
-
autoload :UnsupportedLocale, "#{__dir__}/errors/formatter/unsupported_locale"
|
|
8
6
|
autoload :Numbers, "#{__dir__}/formatter/numbers"
|
|
9
7
|
autoload :Standard, "#{__dir__}/formatter/standard"
|
|
10
8
|
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) |
|