plurimath 0.10.7 → 0.11.0

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +3 -2
  3. data/README.adoc +343 -44
  4. data/lib/plurimath/asciimath/parse.rb +6 -2
  5. data/lib/plurimath/configuration.rb +17 -0
  6. data/lib/plurimath/deprecation.rb +81 -0
  7. data/lib/plurimath/errors/configuration_error.rb +27 -0
  8. data/lib/plurimath/errors/deprecation_error.rb +33 -0
  9. data/lib/plurimath/errors/error.rb +6 -0
  10. data/lib/plurimath/errors/formatter/unsupported_base.rb +1 -1
  11. data/lib/plurimath/errors/formatter/unsupported_locale.rb +18 -0
  12. data/lib/plurimath/errors/omml/unsupported_node_error.rb +1 -1
  13. data/lib/plurimath/errors/parse_error.rb +1 -1
  14. data/lib/plurimath/errors/parse_option_error.rb +34 -0
  15. data/lib/plurimath/formatter/numbers/base.rb +18 -8
  16. data/lib/plurimath/formatter/numbers/base_notation.rb +67 -0
  17. data/lib/plurimath/formatter/numbers/digit_sequence.rb +96 -0
  18. data/lib/plurimath/formatter/numbers/format_options.rb +141 -0
  19. data/lib/plurimath/formatter/numbers/fraction.rb +50 -93
  20. data/lib/plurimath/formatter/numbers/integer.rb +30 -6
  21. data/lib/plurimath/formatter/numbers/notation_renderer.rb +128 -0
  22. data/lib/plurimath/formatter/numbers/number_renderer.rb +66 -0
  23. data/lib/plurimath/formatter/numbers/parts.rb +69 -0
  24. data/lib/plurimath/formatter/numbers/parts_renderer.rb +30 -0
  25. data/lib/plurimath/formatter/numbers/precision_resolver.rb +54 -0
  26. data/lib/plurimath/formatter/numbers/sign_renderer.rb +28 -0
  27. data/lib/plurimath/formatter/numbers/significant.rb +77 -103
  28. data/lib/plurimath/formatter/numbers/source.rb +120 -0
  29. data/lib/plurimath/formatter/numbers/symbol_resolver.rb +55 -0
  30. data/lib/plurimath/formatter/numbers.rb +11 -0
  31. data/lib/plurimath/formatter/standard.rb +32 -42
  32. data/lib/plurimath/formatter/supported_locales.rb +27 -0
  33. data/lib/plurimath/formatter.rb +1 -2
  34. data/lib/plurimath/html/constants.rb +2 -0
  35. data/lib/plurimath/html/parse.rb +77 -14
  36. data/lib/plurimath/html/parser.rb +15 -3
  37. data/lib/plurimath/html/transform.rb +193 -91
  38. data/lib/plurimath/html/transform_utility.rb +61 -0
  39. data/lib/plurimath/html.rb +1 -0
  40. data/lib/plurimath/latex/parse.rb +7 -1
  41. data/lib/plurimath/latex/transform.rb +5 -5
  42. data/lib/plurimath/math/function/lim.rb +6 -0
  43. data/lib/plurimath/math/number.rb +8 -2
  44. data/lib/plurimath/math/symbols/cdot.rb +1 -1
  45. data/lib/plurimath/math/symbols/exclam.rb +1 -1
  46. data/lib/plurimath/math/symbols/minus.rb +1 -1
  47. data/lib/plurimath/math/symbols/percent.rb +1 -1
  48. data/lib/plurimath/math/symbols/pi.rb +1 -1
  49. data/lib/plurimath/math/symbols/slash.rb +1 -1
  50. data/lib/plurimath/math.rb +56 -8
  51. data/lib/plurimath/number_formatter.rb +57 -27
  52. data/lib/plurimath/unicode_math/parse.rb +7 -1
  53. data/lib/plurimath/unicode_math/transform.rb +2 -2
  54. data/lib/plurimath/version.rb +1 -1
  55. data/lib/plurimath.rb +23 -1
  56. metadata +21 -4
  57. data/lib/plurimath/formatter/number_formatter.rb +0 -115
  58. data/lib/plurimath/formatter/numeric_formatter.rb +0 -187
@@ -3,132 +3,121 @@
3
3
  module Plurimath
4
4
  module Formatter
5
5
  module Numbers
6
+ # Applies significant-digit rounding on Parts before localized rendering.
6
7
  class Significant < Base
7
- attr_accessor :decimal, :significant
8
+ attr_reader :significant
8
9
 
9
- def initialize(symbols)
10
+ CANONICAL_DECIMAL = "."
11
+ EMPTY_STRING = ""
12
+ ZERO = "0"
13
+
14
+ def initialize(options)
10
15
  super
11
- @decimal = symbols[:decimal]
12
- @significant = symbols[:significant].to_i
16
+ @significant = self.options.significant
13
17
  end
14
18
 
15
- def apply(string, int_format, frac_format)
16
- return string if significant.zero?
17
-
18
- # Check if string contains any non-zero digit (works across all bases 2-16)
19
- chars = string.chars
20
- return string if skip_significant_processing?(chars)
19
+ def active?
20
+ significant.positive?
21
+ end
21
22
 
22
- string = signify(chars)
23
- integer, fraction = string.split(decimal)
24
- string = [format_groups(int_format, integer)]
25
- string << format_groups(frac_format, fraction) if fraction
26
- string.join(decimal)
23
+ def apply_parts(parts)
24
+ significant_parts(parts)
27
25
  end
28
26
 
29
27
  protected
30
28
 
29
+ # Apply significant-digit rules before localization so rounding never
30
+ # reparses grouped or decimal-localized output.
31
+ def significant_parts(parts)
32
+ integer = parts.integer_digits
33
+ fraction = parts.fraction_digits
34
+ string = fraction.empty? ? integer : "#{integer}#{CANONICAL_DECIMAL}#{fraction}"
35
+ chars = string.chars
36
+ return parts if skip_significant_processing?(chars)
37
+
38
+ integer, fraction = signify(chars).split(CANONICAL_DECIMAL, 2)
39
+ parts.with_digits(
40
+ integer_digits: integer,
41
+ fraction_digits: fraction.to_s,
42
+ )
43
+ end
44
+
31
45
  def signify(chars)
32
46
  new_chars, frac_part, sig_count = process_chars(chars)
33
47
  if sig_count.positive?
34
- new_chars << decimal unless frac_part
48
+ new_chars << CANONICAL_DECIMAL unless frac_part
35
49
  else
36
- remain_chars = count_chars(chars, frac_part) - significant
50
+ remain_chars = digit_count(chars, fraction: frac_part) - significant
37
51
  if remain_chars.positive?
38
- round_str(chars, new_chars, frac_part)
39
- # After rounding, recalculate remain_chars only for fractional numbers
40
- # where we need to adjust padding based on actual significant digits
41
- if frac_part
42
- # Check if decimal point still exists after rounding
43
- has_decimal = new_chars.include?(decimal)
44
- if has_decimal
45
- # Fractional part still exists, recalculate padding
46
- actual_sig = count_significant_digits(new_chars)
47
- remain_chars = [significant - actual_sig, 0].max
48
- else
49
- # Rounding eliminated the fractional part from a number that originally had one
50
- # Don't add trailing zeros in this case
51
- remain_chars = 0
52
- frac_part = false
53
- end
54
- end
55
- # For integer-only numbers (frac_part = false from the start),
56
- # remain_chars stays as calculated initially
52
+ round_chars(chars, new_chars, frac_part)
53
+ remain_chars = remaining_fraction_chars(new_chars) if frac_part
57
54
  end
58
- new_chars << ("0" * remain_chars) unless frac_part && sig_char_count?(new_chars)
55
+ new_chars << (ZERO * remain_chars) unless frac_part && sig_char_count?(new_chars)
59
56
  end
60
57
  new_chars.join
61
58
  end
62
59
 
63
- def round_str(chars, array, frac_part)
64
- arr_len = array.length
65
- char_ind = DIGIT_VALUE.key?(chars[arr_len]) ? arr_len : arr_len.next
66
- return unless char_ind < chars.length && DIGIT_VALUE[chars[char_ind]] >= threshold
67
-
68
- frac_part = false if chars[arr_len] == decimal
69
- carry = false
70
- array.reverse!.each_with_index do |char, ind|
71
- if char == decimal
72
- array[ind] = ""
73
- frac_part = false
74
- next
75
- end
76
- next unless DIGIT_VALUE.key?(char)
77
-
78
- if DIGIT_VALUE[char] == base.pred
79
- carry = true
80
- array[ind] = frac_part ? "" : "0"
81
- else
82
- array[ind] = next_mapping_char(char)
83
- carry = false
84
- break
85
- end
86
- end
87
- array << "1" if carry
88
- array.reverse!
60
+ def round_chars(chars, result, frac_part)
61
+ char_index = round_char_index(chars, result.length)
62
+ return unless char_index < chars.length && digit_sequence.round_up?(chars[char_index])
63
+
64
+ rounded = if frac_part
65
+ increment_fractional(result.reverse)
66
+ else
67
+ increment_integer(result.reverse)
68
+ end
69
+ result.replace(rounded.reverse)
89
70
  end
90
71
 
91
- def count_chars(chars, fraction)
92
- char_count = 0
93
- chars.each do |char|
94
- break if char == decimal && !fraction
72
+ def increment_fractional(reversed)
73
+ decimal_index = reversed.index(CANONICAL_DECIMAL)
74
+ return increment_integer(reversed) unless decimal_index
95
75
 
96
- char_count += 1 if DIGIT_VALUE.key?(char)
97
- end
98
- char_count
76
+ fraction = reversed[0...decimal_index]
77
+ integer = reversed[(decimal_index + 1)..]
78
+ fraction, carry = digit_sequence.increment_reversed(fraction, overflow: EMPTY_STRING)
79
+ return fraction + [CANONICAL_DECIMAL] + integer unless carry.positive?
80
+
81
+ integer = increment_integer(integer)
82
+ fraction + [EMPTY_STRING] + integer
99
83
  end
100
84
 
101
- def format_groups(format, string)
102
- format.format_groups(numeric_string(string, format))
85
+ def increment_integer(reversed)
86
+ digits, carry = digit_sequence.increment_reversed(reversed, overflow: ZERO)
87
+ digits << "1" if carry.positive?
88
+ digits
103
89
  end
104
90
 
105
- def numeric_string(string, format)
106
- string.split(format.separator).join
91
+ def round_char_index(chars, result_length)
92
+ digit_sequence.digit?(chars[result_length]) ? result_length : result_length.next
107
93
  end
108
94
 
109
- def sig_char_count?(chars)
110
- start_counting = false
111
- char_count = 0
112
- chars.each do |char|
113
- start_counting = true if DIGIT_VALUE[char]&.positive?
114
- next unless start_counting
95
+ def remaining_fraction_chars(chars)
96
+ return 0 unless chars.include?(CANONICAL_DECIMAL)
115
97
 
116
- char_count += 1 if DIGIT_VALUE.key?(char)
117
- end
118
- char_count == significant
98
+ [significant - digit_sequence.significant_digit_count(chars), 0].max
99
+ end
100
+
101
+ def digit_count(chars, fraction:)
102
+ stop_at = fraction ? nil : CANONICAL_DECIMAL
103
+ digit_sequence.digit_count(chars, stop_at: stop_at)
104
+ end
105
+
106
+ def sig_char_count?(chars)
107
+ digit_sequence.significant_digit_count(chars) == significant
119
108
  end
120
109
 
121
110
  def process_chars(chars, sig_num: false, frac_part: false)
122
111
  sig_count = significant
123
112
  new_chars = []
124
113
  chars.each do |char|
125
- frac_part ||= char == decimal
126
- sig_num ||= DIGIT_VALUE[char]&.positive?
114
+ frac_part ||= char == CANONICAL_DECIMAL
115
+ sig_num ||= digit_sequence.significant?(char)
127
116
  break if sig_count.zero?
128
117
 
129
118
  new_chars << char
130
119
  next unless sig_num
131
- next unless DIGIT_VALUE.key?(char)
120
+ next unless digit_sequence.digit?(char)
132
121
 
133
122
  sig_count -= 1
134
123
  end
@@ -137,23 +126,8 @@ module Plurimath
137
126
  end
138
127
 
139
128
  def skip_significant_processing?(chars)
140
- # Skip if no significant digits exist, or if we already have the exact count needed
141
- chars.none? { |c| DIGIT_VALUE.key?(c) && DIGIT_VALUE[c].positive? } ||
142
- count_chars(chars, true) == significant
143
- end
144
-
145
- def count_significant_digits(chars)
146
- # Count actual significant digits in the character array
147
- # Leading zeros don't count as significant
148
- start_counting = false
149
- char_count = 0
150
- chars.each do |char|
151
- start_counting = true if DIGIT_VALUE[char]&.positive?
152
- next unless start_counting
153
-
154
- char_count += 1 if DIGIT_VALUE.key?(char)
155
- end
156
- char_count
129
+ digit_sequence.significant_digit_count(chars).zero? ||
130
+ digit_count(chars, fraction: true) == significant
157
131
  end
158
132
  end
159
133
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Plurimath
6
+ module Formatter
7
+ module Numbers
8
+ # Captures raw input, BigDecimal interpretation, and source digit metadata
9
+ # before formatter transforms run.
10
+ class Source
11
+ attr_reader :decimal, :exponent, :exponent_text, :fraction_digits,
12
+ :integer_digits, :raw, :sign
13
+
14
+ DEFAULT_INTEGER = "0"
15
+ EMPTY_STRING = ""
16
+
17
+ def initialize(value)
18
+ @raw = value.to_s
19
+ @decimal = BigDecimal(raw)
20
+ @sign = raw.start_with?("-") ? -1 : 1
21
+
22
+ mantissa, @exponent_text = unsigned_value.split("e", 2)
23
+ @exponent = exponent_text.to_i
24
+ @integer_digits, @fraction_digits = split_mantissa(mantissa)
25
+ end
26
+
27
+ def fractional?
28
+ fraction_digits.length > exponent
29
+ end
30
+
31
+ def decimal_precision
32
+ decimal_digits.last.length
33
+ end
34
+
35
+ def notation_precision
36
+ precision = integer_digits.length + fraction_digits.length - 1
37
+ precision += 1 if sign.negative?
38
+ [precision, 0].max
39
+ end
40
+
41
+ def significant_digit_count
42
+ significant_digits.length
43
+ end
44
+
45
+ def target_base_integer_length(base)
46
+ return decimal_parts_integer_length if base == Base::DEFAULT_BASE
47
+
48
+ integer = decimal.abs.to_i
49
+ return 0 if integer.zero?
50
+
51
+ integer.to_s(base).length
52
+ end
53
+
54
+ def to_parts(base: nil, precision: nil)
55
+ integer, fraction = decimal_digits
56
+ fraction = apply_precision(fraction, precision)
57
+
58
+ Parts.new(
59
+ sign: sign,
60
+ base: base || Base::DEFAULT_BASE,
61
+ integer_digits: integer,
62
+ fraction_digits: fraction,
63
+ )
64
+ end
65
+
66
+ def trailing_fraction_zero_count
67
+ return 0 if fraction_digits.empty?
68
+
69
+ fraction_digits.length - fraction_digits.sub(/0+\z/, "").length
70
+ end
71
+
72
+ private
73
+
74
+ def decimal_parts_integer_length
75
+ parts = to_parts
76
+ return 0 if parts.integer_zero?
77
+
78
+ parts.integer_digits.length
79
+ end
80
+
81
+ def apply_precision(fraction, precision)
82
+ return fraction unless precision
83
+
84
+ size = precision.to_i
85
+ return EMPTY_STRING unless size.positive?
86
+
87
+ fraction[0...size]
88
+ end
89
+
90
+ def decimal_digits
91
+ digits = "#{integer_digits}#{fraction_digits}"
92
+ decimal_index = integer_digits.length + exponent
93
+
94
+ if decimal_index <= 0
95
+ [DEFAULT_INTEGER, "#{'0' * decimal_index.abs}#{digits}"]
96
+ elsif decimal_index >= digits.length
97
+ ["#{digits}#{'0' * (decimal_index - digits.length)}", EMPTY_STRING]
98
+ else
99
+ [digits[0...decimal_index], digits[decimal_index..]]
100
+ end
101
+ end
102
+
103
+ def significant_digits
104
+ "#{integer_digits}#{fraction_digits}".sub(/\A0+/, "")
105
+ end
106
+
107
+ def split_mantissa(mantissa)
108
+ integer, fraction = mantissa.split(".", 2)
109
+ integer = DEFAULT_INTEGER if integer.to_s.empty?
110
+
111
+ [integer, fraction.to_s]
112
+ end
113
+
114
+ def unsigned_value
115
+ raw.downcase.sub(/\A[-+]/, "")
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Formatter
5
+ module Numbers
6
+ # Merges locale defaults, explicit symbols, and localize_number templates
7
+ # into a fresh symbol hash for one formatter call.
8
+ class SymbolResolver
9
+ LOCALIZE_NUMBER_REGEX = %r{(?<group>[^#])?(?<groupdigits>#+0)(?<decimal>.)(?<fractdigits>#+)(?<fractgroup>[^#])?}
10
+
11
+ def initialize(locale, localizer_symbols:, localize_number:)
12
+ @locale = locale
13
+ @localizer_symbols = localizer_symbols
14
+ @localize_number = localize_number
15
+ end
16
+
17
+ def resolve
18
+ locale_symbols
19
+ .merge(localizer_symbols_hash)
20
+ .merge(localize_number_symbols)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :locale, :localizer_symbols, :localize_number
26
+
27
+ def locale_symbols
28
+ Formatter::SupportedLocales::LOCALES[locale.to_sym].dup
29
+ end
30
+
31
+ def localizer_symbols_hash
32
+ return {} unless localizer_symbols
33
+
34
+ localizer_symbols.respond_to?(:to_h) ? localizer_symbols.to_h : localizer_symbols
35
+ end
36
+
37
+ def localize_number_symbols
38
+ localize_number or return {}
39
+ match = LOCALIZE_NUMBER_REGEX.match(localize_number) or return {}
40
+ {
41
+ decimal: match[:decimal],
42
+ group_digits: match[:groupdigits].size,
43
+ fraction_group_digits: match[:fractdigits].size,
44
+ group: normalize_space(match[:group] || ""),
45
+ fraction_group: normalize_space(match[:fractgroup] || ""),
46
+ }.compact
47
+ end
48
+
49
+ def normalize_space(value)
50
+ value == " " ? "\u00A0" : value
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -4,9 +4,20 @@ 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
+ autoload :BaseNotation, "#{__dir__}/numbers/base_notation"
7
9
  autoload :Fraction, "#{__dir__}/numbers/fraction"
10
+ autoload :FormatOptions, "#{__dir__}/numbers/format_options"
8
11
  autoload :Integer, "#{__dir__}/numbers/integer"
12
+ autoload :NumberRenderer, "#{__dir__}/numbers/number_renderer"
13
+ autoload :NotationRenderer, "#{__dir__}/numbers/notation_renderer"
14
+ autoload :Parts, "#{__dir__}/numbers/parts"
15
+ autoload :PartsRenderer, "#{__dir__}/numbers/parts_renderer"
16
+ autoload :PrecisionResolver, "#{__dir__}/numbers/precision_resolver"
17
+ autoload :SignRenderer, "#{__dir__}/numbers/sign_renderer"
9
18
  autoload :Significant, "#{__dir__}/numbers/significant"
19
+ autoload :Source, "#{__dir__}/numbers/source"
20
+ autoload :SymbolResolver, "#{__dir__}/numbers/symbol_resolver"
10
21
  end
11
22
  end
12
23
  end
@@ -3,14 +3,13 @@
3
3
  module Plurimath
4
4
  module Formatter
5
5
  class Standard < Plurimath::NumberFormatter
6
- attr_accessor :precision
7
-
8
6
  DEFAULT_OPTIONS = {
9
7
  fraction_group_digits: 3,
10
8
  exponent_sign: nil,
11
9
  fraction_group: "'",
12
10
  number_sign: nil,
13
11
  notation: :basic,
12
+ padding: "0",
14
13
  group_digits: 3,
15
14
  significant: 0,
16
15
  digit_count: 0,
@@ -21,8 +20,12 @@ module Plurimath
21
20
  e: "e",
22
21
  }.freeze
23
22
 
24
- def initialize(locale: "en", string_format: nil, options: {},
25
- precision: nil)
23
+ def initialize(
24
+ locale: "en",
25
+ string_format: nil,
26
+ options: {},
27
+ precision: nil
28
+ )
26
29
  super(
27
30
  locale,
28
31
  localize_number: string_format,
@@ -32,46 +35,33 @@ precision: nil)
32
35
  end
33
36
 
34
37
  def set_default_options(options)
35
- default_options = self.class::DEFAULT_OPTIONS
36
- self.precision ||= default_options[:precision]
37
- options ||= default_options
38
- unless options.key?(:fraction_group_digits)
39
- options[:fraction_group_digits] =
40
- default_options[:fraction_group_digits]
41
- end
42
- unless options.key?(:fraction_group)
43
- options[:fraction_group] =
44
- default_options[:fraction_group]
45
- end
46
- unless options.key?(:exponent_sign)
47
- options[:exponent_sign] =
48
- default_options[:exponent_sign]
49
- end
50
- unless options.key?(:group_digits)
51
- options[:group_digits] =
52
- default_options[:group_digits]
53
- end
54
- unless options.key?(:number_sign)
55
- options[:number_sign] =
56
- default_options[:number_sign]
57
- end
58
- unless options.key?(:significant)
59
- options[:significant] =
60
- default_options[:significant]
61
- end
62
- unless options.key?(:notation)
63
- options[:notation] =
64
- default_options[:notation]
65
- end
66
- unless options.key?(:decimal)
67
- options[:decimal] =
68
- default_options[:decimal]
69
- end
70
- options[:group] = default_options[:group] unless options.key?(:group)
71
- options[:times] = default_options[:times] unless options.key?(:times)
72
- options[:e] = default_options[:e] unless options.key?(:e)
38
+ options = options ? options.dup : {}
39
+ apply_default_symbols(options)
73
40
  options
74
41
  end
42
+
43
+ private
44
+
45
+ def apply_default_symbols(options)
46
+ %i[
47
+ fraction_group_digits
48
+ fraction_group
49
+ exponent_sign
50
+ group_digits
51
+ padding
52
+ number_sign
53
+ significant
54
+ notation
55
+ decimal
56
+ group
57
+ times
58
+ e
59
+ ].each { |key| default_key(options, key) }
60
+ end
61
+
62
+ def default_key(options, key)
63
+ options[key] = self.class::DEFAULT_OPTIONS[key] unless options.key?(key)
64
+ end
75
65
  end
76
66
  end
77
67
  end
@@ -101,6 +101,33 @@ module Plurimath
101
101
  zh: { decimal: ".", group: "," },
102
102
  zu: { decimal: ".", group: "," },
103
103
  }.freeze
104
+
105
+ class << self
106
+ def decimal_for(locale, default:)
107
+ symbols_for(locale).fetch(:decimal, default)
108
+ end
109
+
110
+ def symbols_for(locale)
111
+ locale_key = key_for(locale)
112
+ return {} unless locale_key
113
+
114
+ LOCALES.fetch(locale_key)
115
+ end
116
+
117
+ def key_for!(locale)
118
+ locale_key = key_for(locale)
119
+ return locale_key if locale.nil? || locale_key
120
+
121
+ raise UnsupportedLocale.new(locale, LOCALES.keys)
122
+ end
123
+
124
+ def key_for(locale)
125
+ return locale if LOCALES.key?(locale)
126
+
127
+ symbol_key = locale.to_sym if locale.respond_to?(:to_sym)
128
+ symbol_key if LOCALES.key?(symbol_key)
129
+ end
130
+ end
104
131
  end
105
132
  end
106
133
  end
@@ -2,10 +2,9 @@
2
2
 
3
3
  module Plurimath
4
4
  module Formatter
5
- autoload :NumericFormatter, "#{__dir__}/formatter/numeric_formatter"
6
- autoload :NumberFormatter, "#{__dir__}/formatter/number_formatter"
7
5
  autoload :SupportedLocales, "#{__dir__}/formatter/supported_locales"
8
6
  autoload :UnsupportedBase, "#{__dir__}/errors/formatter/unsupported_base"
7
+ autoload :UnsupportedLocale, "#{__dir__}/errors/formatter/unsupported_locale"
9
8
  autoload :Numbers, "#{__dir__}/formatter/numbers"
10
9
  autoload :Standard, "#{__dir__}/formatter/standard"
11
10
  end
@@ -38,6 +38,8 @@ module Plurimath
38
38
  SUB_SUP_CLASSES = {
39
39
  "&prod;": :prod,
40
40
  "&sum;": :sum,
41
+ "&#x220f;": :prod,
42
+ "&#x2211;": :sum,
41
43
  log: :log,
42
44
  lim: :lim,
43
45
  "∏": :prod,