plurimath 0.11.0 → 0.11.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +354 -34
  3. data/Gemfile +2 -2
  4. data/README.adoc +49 -9
  5. data/lib/plurimath/deprecation.rb +2 -1
  6. data/lib/plurimath/errors/configuration_error.rb +5 -1
  7. data/lib/plurimath/errors/deprecation_error.rb +2 -1
  8. data/lib/plurimath/errors/invalid_number.rb +17 -0
  9. data/lib/plurimath/errors/unsupported_base.rb +18 -0
  10. data/lib/plurimath/errors/{formatter/unsupported_locale.rb → unsupported_locale.rb} +1 -1
  11. data/lib/plurimath/errors.rb +8 -0
  12. data/lib/plurimath/formatter/numbers/base_notation.rb +7 -2
  13. data/lib/plurimath/formatter/numbers/format_options.rb +123 -17
  14. data/lib/plurimath/formatter/numbers/fraction.rb +7 -16
  15. data/lib/plurimath/formatter/numbers/integer.rb +3 -0
  16. data/lib/plurimath/formatter/numbers/notation_renderer.rb +35 -6
  17. data/lib/plurimath/formatter/numbers/number_renderer.rb +16 -4
  18. data/lib/plurimath/formatter/numbers/precision_resolver.rb +16 -3
  19. data/lib/plurimath/formatter/numbers/sign_renderer.rb +2 -1
  20. data/lib/plurimath/formatter/numbers/significant.rb +4 -2
  21. data/lib/plurimath/formatter/numbers/source.rb +18 -3
  22. data/lib/plurimath/formatter/standard.rb +6 -0
  23. data/lib/plurimath/formatter/supported_locales.rb +1 -1
  24. data/lib/plurimath/formatter.rb +0 -2
  25. data/lib/plurimath/html/transform.rb +24 -12
  26. data/lib/plurimath/math/core.rb +17 -14
  27. data/lib/plurimath/math/function/overleftrightarrow.rb +2 -2
  28. data/lib/plurimath/math/number.rb +11 -1
  29. data/lib/plurimath/math.rb +4 -1
  30. data/lib/plurimath/number_formatter.rb +45 -4
  31. data/lib/plurimath/omml/formula_transformation.rb +2 -1
  32. data/lib/plurimath/omml/translator.rb +4 -1
  33. data/lib/plurimath/unicode_math/parse.rb +93 -53
  34. data/lib/plurimath/unicode_math/parser.rb +7 -4
  35. data/lib/plurimath/unicode_math/parsing_rules/absence_rules.rb +33 -21
  36. data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +28 -16
  37. data/lib/plurimath/unicode_math/parsing_rules/constants_rules.rb +18 -7
  38. data/lib/plurimath/unicode_math/parsing_rules/masked.rb +35 -15
  39. data/lib/plurimath/unicode_math/parsing_rules/sub_sup.rb +98 -72
  40. data/lib/plurimath/unicode_math/transform.rb +755 -468
  41. data/lib/plurimath/version.rb +1 -1
  42. data/lib/plurimath.rb +1 -0
  43. metadata +5 -4
  44. data/lib/plurimath/errors/formatter/unsupported_base.rb +0 -21
@@ -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,9 +13,13 @@ 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
20
  def initialize(value)
18
21
  @raw = value.to_s
22
+ validate_numeric!(value)
19
23
  @decimal = BigDecimal(raw)
20
24
  @sign = raw.start_with?("-") ? -1 : 1
21
25
 
@@ -33,9 +37,13 @@ module Plurimath
33
37
  end
34
38
 
35
39
  def notation_precision
36
- precision = integer_digits.length + fraction_digits.length - 1
37
- precision += 1 if sign.negative?
38
- [precision, 0].max
40
+ # A zero coefficient keeps the source's stated fraction width.
41
+ return fraction_digits.length if decimal.zero?
42
+
43
+ # The coefficient's fraction width is its significant-digit count
44
+ # minus the single leading digit; the sign and leading zeros carry
45
+ # no precision.
46
+ [significant_digit_count - 1, 0].max
39
47
  end
40
48
 
41
49
  def significant_digit_count
@@ -71,6 +79,13 @@ module Plurimath
71
79
 
72
80
  private
73
81
 
82
+ def validate_numeric!(value)
83
+ valid_type = value.is_a?(Numeric) || value.is_a?(String)
84
+ return if valid_type && NUMERIC_PATTERN.match?(raw)
85
+
86
+ raise Plurimath::Errors::InvalidNumber, value
87
+ end
88
+
74
89
  def decimal_parts_integer_length
75
90
  parts = to_parts
76
91
  return 0 if parts.integer_zero?
@@ -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
@@ -202,25 +202,29 @@ module Plurimath
202
202
  rule(sub_sup: simple(:sub_sup),
203
203
  sub_value: simple(:sub_value),
204
204
  sup_value: simple(:sup_value)) do
205
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value)
205
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
206
+ sup_value: sup_value)
206
207
  end
207
208
 
208
209
  rule(sub_sup: simple(:sub_sup),
209
210
  sub_value: simple(:sub_value),
210
211
  sup_value: sequence(:sup_value)) do
211
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value)
212
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
213
+ sup_value: sup_value)
212
214
  end
213
215
 
214
216
  rule(sub_sup: simple(:sub_sup),
215
217
  sub_value: sequence(:sub_value),
216
218
  sup_value: simple(:sup_value)) do
217
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value)
219
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
220
+ sup_value: sup_value)
218
221
  end
219
222
 
220
223
  rule(sub_sup: simple(:sub_sup),
221
224
  sub_value: sequence(:sub_value),
222
225
  sup_value: sequence(:sup_value)) do
223
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value)
226
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
227
+ sup_value: sup_value)
224
228
  end
225
229
 
226
230
  # Parslet emits separate shapes for each sub/sup value combination and
@@ -303,7 +307,8 @@ module Plurimath
303
307
  sup_value: simple(:sup_value),
304
308
  expression: simple(:expression)) do
305
309
  TransformUtility.append_expression(
306
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
310
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
311
+ sup_value: sup_value),
307
312
  expression,
308
313
  )
309
314
  end
@@ -313,7 +318,8 @@ module Plurimath
313
318
  sup_value: simple(:sup_value),
314
319
  expression: sequence(:expression)) do
315
320
  TransformUtility.append_expression(
316
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
321
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
322
+ sup_value: sup_value),
317
323
  expression,
318
324
  )
319
325
  end
@@ -323,7 +329,8 @@ module Plurimath
323
329
  sup_value: sequence(:sup_value),
324
330
  expression: simple(:expression)) do
325
331
  TransformUtility.append_expression(
326
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
332
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
333
+ sup_value: sup_value),
327
334
  expression,
328
335
  )
329
336
  end
@@ -333,7 +340,8 @@ module Plurimath
333
340
  sup_value: sequence(:sup_value),
334
341
  expression: sequence(:expression)) do
335
342
  TransformUtility.append_expression(
336
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
343
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
344
+ sup_value: sup_value),
337
345
  expression,
338
346
  )
339
347
  end
@@ -343,7 +351,8 @@ module Plurimath
343
351
  sup_value: simple(:sup_value),
344
352
  expression: simple(:expression)) do
345
353
  TransformUtility.append_expression(
346
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
354
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
355
+ sup_value: sup_value),
347
356
  expression,
348
357
  )
349
358
  end
@@ -353,7 +362,8 @@ module Plurimath
353
362
  sup_value: simple(:sup_value),
354
363
  expression: sequence(:expression)) do
355
364
  TransformUtility.append_expression(
356
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
365
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
366
+ sup_value: sup_value),
357
367
  expression,
358
368
  )
359
369
  end
@@ -363,7 +373,8 @@ module Plurimath
363
373
  sup_value: sequence(:sup_value),
364
374
  expression: simple(:expression)) do
365
375
  TransformUtility.append_expression(
366
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
376
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
377
+ sup_value: sup_value),
367
378
  expression,
368
379
  )
369
380
  end
@@ -373,7 +384,8 @@ module Plurimath
373
384
  sup_value: sequence(:sup_value),
374
385
  expression: sequence(:expression)) do
375
386
  TransformUtility.append_expression(
376
- TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value, sup_value: sup_value),
387
+ TransformUtility.sub_sup_value(sub_sup, sub_value: sub_value,
388
+ sup_value: sup_value),
377
389
  expression,
378
390
  )
379
391
  end
@@ -53,14 +53,15 @@ module Plurimath
53
53
  wrapper_tag << r_tag
54
54
  end
55
55
 
56
- def omml_parameter(field, display_style, tag_name:, options:, namespace: "m")
56
+ def omml_parameter(field, display_style, tag_name:, options:,
57
+ namespace: "m")
57
58
  tag = ox_element(tag_name, namespace: namespace)
58
59
  return empty_tag(tag) unless field
59
60
 
60
61
  field_value = if field.is_a?(Array)
61
- field.map { |object|
62
- object.insert_t_tag(display_style, options: options)
63
- }
62
+ field.map do |object|
63
+ object.insert_t_tag(display_style, options: options)
64
+ end
64
65
  else
65
66
  field.insert_t_tag(display_style, options: options)
66
67
  end
@@ -106,7 +107,7 @@ module Plurimath
106
107
 
107
108
  options[:array] << field&.to_asciimath_math_zone(
108
109
  hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
109
- )
110
+ )
110
111
  end
111
112
 
112
113
  def latex_fields_to_print(field, options = {})
@@ -131,7 +132,7 @@ module Plurimath
131
132
 
132
133
  options[:array] << field&.to_mathml_math_zone(
133
134
  hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
134
- )
135
+ )
135
136
  end
136
137
 
137
138
  def omml_fields_to_print(field, options = {})
@@ -156,13 +157,13 @@ module Plurimath
156
157
 
157
158
  options[:array] << field&.to_unicodemath_math_zone(
158
159
  hashed[:function_spacing], hashed[:last], hashed[:indent], options: options[:options]
159
- )
160
+ )
160
161
  end
161
162
 
162
163
  def dump_mathml(field, intent = false, options:)
163
164
  dump_ox_nodes(field.mathml_nodes(intent, options: options)).gsub(
164
165
  /\n\s*/, ""
165
- )
166
+ )
166
167
  end
167
168
 
168
169
  def dump_omml(field, display_style, options:)
@@ -170,7 +171,7 @@ module Plurimath
170
171
 
171
172
  dump_ox_nodes(field.omml_nodes(display_style, options: options)).gsub(
172
173
  /\n\s*/, ""
173
- )
174
+ )
174
175
  end
175
176
 
176
177
  def mathml_nodes(intent, options:)
@@ -260,8 +261,10 @@ module Plurimath
260
261
  case field
261
262
  when Core
262
263
  field.line_breaking(obj)
263
- updated_object_values(variable, obj: obj,
264
- update_value: true) if obj.value_exist?
264
+ if obj.value_exist?
265
+ updated_object_values(variable, obj: obj,
266
+ update_value: true)
267
+ end
265
268
  when Array
266
269
  array_line_break_field(field, variable, obj)
267
270
  end
@@ -361,9 +364,9 @@ module Plurimath
361
364
  return false unless field.is_a?(Math::Symbols::Symbol)
362
365
  return true if field&.value&.include?("&#x27;")
363
366
 
364
- Utility.primes_constants.any? { |_prefix, prime|
365
- unicodemath_field_value(field).include?(prime)
366
- }
367
+ Utility.primes_constants.any? do |_prefix, prime|
368
+ unicodemath_field_value(field).include?(prime)
369
+ end
367
370
  end
368
371
 
369
372
  def paren?
@@ -14,7 +14,7 @@ module Plurimath
14
14
  def to_mathml_without_math_tag(intent, options:)
15
15
  mover = ox_element("mover")
16
16
  first_value = parameter_one&.to_mathml_without_math_tag(intent,
17
- options: options)
17
+ options: options)
18
18
  if attributes && attributes[:accent]
19
19
  mover[:accent] =
20
20
  attributes[:accent]
@@ -36,7 +36,7 @@ module Plurimath
36
36
  else
37
37
  symbol = Symbols::Symbol.new("\u20e1")
38
38
  Overset.new(parameter_one, symbol).to_omml_without_math_tag(true,
39
- options: options)
39
+ options: options)
40
40
  end
41
41
  end
42
42
 
@@ -103,11 +103,21 @@ module Plurimath
103
103
  elsif formatter.respond_to?(:format)
104
104
  formatter.format(options[:formula], self)
105
105
  elsif formatter.respond_to?(:localized_number)
106
- formatter.localized_number(value.to_s)
106
+ formatter.localized_number(value.to_s,
107
+ **format_kwargs(options[:format]))
107
108
  else
108
109
  value
109
110
  end
110
111
  end
112
+
113
+ private
114
+
115
+ # Empty splat is silently dropped by Ruby, so simple formatter signatures still work.
116
+ def format_kwargs(format)
117
+ return {} unless format.is_a?(Hash) && !format.empty?
118
+
119
+ { format: format }
120
+ end
111
121
  end
112
122
  end
113
123
  end
@@ -32,7 +32,10 @@ module Plurimath
32
32
  unknown_options = options.keys - SUPPORTED_PARSE_OPTIONS
33
33
  raise_unknown_parse_options!(unknown_options) unless unknown_options.empty?
34
34
 
35
- raise_unsupported_parse_option!(type, :locale) if options.key?(:locale) && !localized_parse_type?(type)
35
+ if options.key?(:locale) && !localized_parse_type?(type)
36
+ raise_unsupported_parse_option!(type,
37
+ :locale)
38
+ end
36
39
  options = normalize_parse_options(options)
37
40
 
38
41
  begin
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Plurimath
4
4
  class NumberFormatter
5
- attr_accessor :locale, :localize_number, :localizer_symbols, :precision
5
+ attr_accessor :locale, :precision, :localize_number, :localizer_symbols
6
6
 
7
7
  def initialize(
8
8
  locale = "en",
@@ -24,7 +24,8 @@ module Plurimath
24
24
  )
25
25
  locale = supported_locale(locale)
26
26
  source = Formatter::Numbers::Source.new(number_string)
27
- options = format_options(source, locale, precision, format)
27
+ options = format_options(source, locale, precision,
28
+ validated_format(format))
28
29
 
29
30
  if options.notation_supported?
30
31
  return notation_renderer(options).render(source, options.notation)
@@ -39,6 +40,28 @@ module Plurimath
39
40
 
40
41
  private
41
42
 
43
+ # localize_number/localizer_symbols are not per-option-hardened — they feed
44
+ # SymbolResolver directly — so validate their types at the point of use (see
45
+ # #symbol_resolver). That single check covers the constructor, the public
46
+ # writers, and any later mutation, raising a clean ConfigurationError
47
+ # instead of leaking a TypeError out of SymbolResolver.
48
+ def validated_localize_number(value)
49
+ return value if value.nil? || value.is_a?(String)
50
+
51
+ raise Plurimath::ConfigurationError.new(
52
+ :invalid_formatter_option, option: :localize_number, value: value, supported: "a String"
53
+ )
54
+ end
55
+
56
+ def validated_localizer_symbols(value)
57
+ return {} if value.nil?
58
+ return value if value.is_a?(Hash)
59
+
60
+ raise Plurimath::ConfigurationError.new(
61
+ :invalid_formatter_option, option: :localizer_symbols, value: value, supported: "a Hash"
62
+ )
63
+ end
64
+
42
65
  def format_options(source, locale, precision, format)
43
66
  Formatter::Numbers::FormatOptions.new(
44
67
  source,
@@ -66,8 +89,8 @@ module Plurimath
66
89
  def symbol_resolver(locale)
67
90
  Formatter::Numbers::SymbolResolver.new(
68
91
  locale,
69
- localizer_symbols: localizer_symbols,
70
- localize_number: localize_number,
92
+ localizer_symbols: validated_localizer_symbols(localizer_symbols),
93
+ localize_number: validated_localize_number(localize_number),
71
94
  )
72
95
  end
73
96
 
@@ -76,7 +99,25 @@ module Plurimath
76
99
  end
77
100
 
78
101
  def supported_locale(locale)
102
+ # Locale always falls back to :en for any unsupported value, including
103
+ # nil and non-string/symbol types; it never raises.
104
+ return :en unless locale.is_a?(String) || locale.is_a?(Symbol)
105
+
79
106
  Formatter::SupportedLocales::LOCALES.key?(locale.to_sym) ? locale.to_sym : :en
80
107
  end
108
+
109
+ def validated_format(format)
110
+ return {} if format.nil?
111
+ unless format.is_a?(Hash)
112
+ raise Plurimath::ConfigurationError.new(
113
+ :invalid_formatter_option,
114
+ option: :format,
115
+ value: format,
116
+ supported: "a Hash of formatter options",
117
+ )
118
+ end
119
+
120
+ format
121
+ end
81
122
  end
82
123
  end
@@ -216,7 +216,8 @@ module Plurimath
216
216
  def accent_value(value)
217
217
  accent = value.first
218
218
  unless accent.is_a?(Math::Function::UnaryFunction)
219
- return Math::Function::Overset.new(accent, value.last, { accent: true })
219
+ return Math::Function::Overset.new(accent, value.last,
220
+ { accent: true })
220
221
  end
221
222
 
222
223
  accent.attributes = { accent: true }
@@ -241,7 +241,10 @@ module Plurimath
241
241
  chr = first_omml_child(pr&.chr)&.val
242
242
  value = omml_argument_value(node.e)
243
243
 
244
- return group_character_above_value(chr, value) if group_character_above?(pr)
244
+ if group_character_above?(pr)
245
+ return group_character_above_value(chr,
246
+ value)
247
+ end
245
248
 
246
249
  group_character_below_value(chr, value)
247
250
  end