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
@@ -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 :exponent_sign_renderer, :options, :precision
33
+ attr_reader :options, :precision
35
34
 
36
35
  def render_e(source)
37
- localized_notation_parts(source).join(options.exponent_separator.to_s)
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
- localized_notation_parts(source).join(" #{options.times} 10^")
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 = engineering_notation_parts(parts) unless source.decimal.zero?
47
- parts[0] = localize_parts(source, parts[0], precision: engineering_precision(source))
48
- parts.join(" #{options.times} 10^")
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
- return [source.to_parts(base: options.base), 0] if source.decimal.zero?
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 engineering_notation_parts(parts)
101
+ def engineering_coefficient_parts(parts)
85
102
  coefficient, exponent = parts
86
103
  index = exponent % 3
87
- exponent -= index
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(integer_length, "0")
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
- render_exponent(exponent),
118
+ new_exponent,
100
119
  ]
101
120
  end
102
121
 
103
- def engineering_precision(source)
104
- return precision if precision.positive?
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
- [source.significant_digit_count - 1, 0].max
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.new(@options)
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
- parts = parts.with_digits(
36
- fraction_digits: parts.fraction_digits[0...precision.to_i].to_s,
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
- sign_renderer.apply(parts, base_notation.apply(result))
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
- :parts_renderer, :significant_format, :sign_renderer
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:, notation_supported:)
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, significant)
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
- return source.notation_precision if notation_supported
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
@@ -20,7 +20,8 @@ module Plurimath
20
20
  def prefix(number)
21
21
  return "-" if number.negative?
22
22
 
23
- "+" if positive_sign&.to_sym == :plus
23
+ # FormatOptions delivers a normalized Symbol (or nil) here.
24
+ "+" if positive_sign == :plus
24
25
  end
25
26
  end
26
27
  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, overflow: EMPTY_STRING)
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, overflow: ZERO)
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
- @decimal = BigDecimal(raw)
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
- precision = integer_digits.length + fraction_digits.length - 1
37
- precision += 1 if sign.negative?
38
- [precision, 0].max
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)
@@ -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
@@ -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) |