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
@@ -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 PartsRenderer boundary.
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 :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(
48
- source,
49
- parts[0],
50
- precision: engineering_precision(source, parts[0]),
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 engineering_notation_parts(parts)
101
+ def engineering_coefficient_parts(parts)
92
102
  coefficient, exponent = parts
93
103
  index = exponent % 3
94
- exponent -= index
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
- render_exponent(exponent),
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.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)
@@ -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
- 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
+ )
47
48
  end
48
49
 
49
50
  private
50
51
 
51
52
  attr_reader :base_notation, :fraction_format, :integer_format,
52
- :parts_renderer, :significant_format, :sign_renderer
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 = BigDecimal(raw)
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
@@ -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)) }