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
data/README.adoc CHANGED
@@ -286,6 +286,83 @@ formula = Plurimath::Math.parse(omml, :omml)
286
286
  ----
287
287
 
288
288
 
289
+ === Evaluating formulas
290
+
291
+ `Plurimath::Math::Formula#evaluate` computes numeric results from supported
292
+ semantic formula trees.
293
+
294
+ [source,ruby]
295
+ ----
296
+ formula = Plurimath::Math.parse("sqrt(a^2+b^2)", :asciimath)
297
+ formula.evaluate(a: 3, b: 4)
298
+ # => 5.0
299
+ ----
300
+
301
+ Variable bindings can use string or symbol keys.
302
+
303
+ [source,ruby]
304
+ ----
305
+ formula = Plurimath::Math.parse("a+b-c*d", :asciimath)
306
+ formula.evaluate("a" => 10, b: 5, c: 2, d: 3)
307
+ # => 9
308
+ ----
309
+
310
+ Evaluation supports numeric literals, variables, addition, subtraction,
311
+ multiplication, division, fractions, powers, grouping, square roots, `pi`,
312
+ roots, absolute values, floor/ceiling, `sin`, `cos`, `tan`, inverse trig,
313
+ reciprocal trig, hyperbolic trig, `ln`, and `exp`. Trigonometric functions use
314
+ radians.
315
+
316
+ Logarithms evaluate with `ln`, `lg`, and `log` — `log` defaults to base 10,
317
+ `log_b` uses the subscript base (which must be positive and not 1), and a
318
+ superscript such as `log_2^3(8)` raises the result to that power. `mod`,
319
+ `max`, `min`, `gcd`, and `lcm` are supported (`gcd`/`lcm` require integer
320
+ arguments), as are bounded `sum` and `prod` with integer bounds — empty
321
+ ranges follow the standard conventions of `0` for sums and `1` for products,
322
+ and the iteration index shadows any outer binding of the same name. Ranges
323
+ are capped at `Plurimath.configuration.evaluation_max_iterations` (default
324
+ 1,000,000) to guard against runaway loops; set it (globally via
325
+ `Plurimath.configure` or scoped via `Plurimath.with_configuration`) to a
326
+ larger value, or to `nil` to remove the cap.
327
+
328
+ `pi` resolves to the constant from each syntax's pi symbol — `pi` in
329
+ AsciiMath, `\pi` in LaTeX, and the `π` symbol (or `π` entity) in the
330
+ other syntaxes. Plain-text `pi` is treated per syntax: a single bindable
331
+ variable in MathML (`<mi>pi</mi>`) and OMML text runs, but individual
332
+ identifiers (`p*i`) in LaTeX, UnicodeMath, and HTML, which tokenize letters
333
+ one by one.
334
+
335
+ Implicit multiplication by juxtaposition is supported for adjacent operands,
336
+ so `2a`, `2(a+b)`, `(a+b)(a-b)`, and `pi r^2` multiply. Two adjacent numeric
337
+ literals such as `2 3` raise instead, because that shape usually indicates a
338
+ split number literal.
339
+
340
+ Chained powers follow the parsed tree structure, so `2^3^2` evaluates as
341
+ `(2^3)^2`; use explicit grouping such as `2^(3^2)` for right association.
342
+ Division uses `Float` arithmetic, and pole detection is exact: `cot(0)` raises
343
+ a division-by-zero error, while `sec(pi/2)` returns a large finite number
344
+ because the floating-point value of `pi/2` is not exactly the pole.
345
+
346
+ Binding values must be real `Numeric` values. A recognized `pi` constant
347
+ always resolves to its numeric value and cannot be overridden by a binding
348
+ such as `pi: 3`.
349
+
350
+ Binding keys must be strings or symbols; other key types raise
351
+ `Plurimath::Math::Evaluation::InvalidBindingKeyError`.
352
+
353
+ Missing variables raise
354
+ `Plurimath::Math::Evaluation::MissingVariableError`. Binding values that are
355
+ not real numbers raise `Plurimath::Math::Evaluation::InvalidBindingError`.
356
+ Division by zero raises `Plurimath::Math::Evaluation::DivisionByZeroError`.
357
+ Math domain violations such as `ln(-1)`, including results that are not real
358
+ numbers, raise `Plurimath::Math::Evaluation::MathDomainError`. Evaluations
359
+ that do not produce a finite number, such as `ln(0)`, raise
360
+ `Plurimath::Math::Evaluation::NonFiniteResultError`. Unsupported expressions,
361
+ including equations that would require solving, raise
362
+ `Plurimath::Math::Evaluation::UnsupportedExpressionError`. All evaluation
363
+ errors subclass `Plurimath::Math::Evaluation::Error`.
364
+
365
+
289
366
  === Converting to other formats
290
367
 
291
368
  Once you have a `Plurimath::Math::Formula` object, you can convert it to
@@ -513,7 +590,7 @@ formatter = Plurimath::NumberFormatter.new(
513
590
  <1> Locale to be used for number formatting.
514
591
  <2> String pattern to define the number formatting.
515
592
  <3> Hash containing relevant options for number formatting.
516
- <4> Number of decimal places to round.
593
+ <4> Number of decimal places to keep; the fraction is truncated, not rounded.
517
594
 
518
595
  Where,
519
596
 
@@ -534,7 +611,8 @@ options for number formatting. Use either `localize_number` or
534
611
  See <<localizer_symbols,format options hash>> for details.
535
612
 
536
613
  `precision: <precision-number>`:: (optional, default `nil`)
537
- Number of decimal places to round. Accepts an integer value.
614
+ Number of decimal places to keep; the fraction is truncated, not
615
+ rounded. Accepts an integer value.
538
616
  +
539
617
  .Specifying a precision of 6 digits
540
618
  [example]
@@ -571,6 +649,28 @@ through a Hash object containing specified options.
571
649
 
572
650
  This Hash object is called the "format options Hash".
573
651
 
652
+ Option values follow a common handling policy:
653
+
654
+ * Count options (`precision`, `significant`, `digit_count`, `group_digits`,
655
+ `fraction_group_digits`, `padding_digits`, `padding_group_digits`) accept an
656
+ `Integer` or its `String`/`Symbol` numeric equivalent (`"4"`). Any other
657
+ value, including negative numbers, raises `Plurimath::ConfigurationError`.
658
+ * Separator options (`decimal`, `group`, `fraction_group`, `base_prefix`,
659
+ `base_postfix`) accept free-form strings. An explicitly passed `nil`
660
+ disables the separator — for `decimal` this renders the digits without a
661
+ decimal mark, and output correctness is then the caller's responsibility.
662
+ One exception: `group: nil` falls back to the default separator instead of
663
+ disabling (use `group: ""` to disable grouping). Boolean values raise
664
+ `Plurimath::ConfigurationError`.
665
+ * Enumerated options (`notation`, `number_sign`, `exponent_sign`, `e`,
666
+ `times`, `hex_capital`) accept their documented values as `Symbol` or
667
+ `String`; unrecognized `String`/`Symbol` values fall back to the default
668
+ behavior. Any other type raises `Plurimath::ConfigurationError` — including
669
+ Boolean, except for `hex_capital`, where `true`/`false` are documented values.
670
+ * The `locale` always falls back to `:en` for unsupported or wrong-typed
671
+ values; it never raises.
672
+ * Invalid number input raises `Plurimath::Errors::InvalidNumber`.
673
+
574
674
  Available options are explained below.
575
675
 
576
676
  NOTE: Each option takes an input of a certain specified type. Using an input
@@ -584,6 +684,15 @@ The same option keys can be passed through `localizer_symbols`, through the
584
684
  per-call `format:` hash, or through `Plurimath::Formatter::Standard`'s
585
685
  `options:` argument.
586
686
 
687
+ When rendering through `Plurimath::Math::Formula` serialization methods
688
+ (`to_asciimath`, `to_latex`, `to_mathml`, `to_unicodemath`, `to_html`),
689
+ per-element overrides can be supplied via `options[:format]`. The hash is
690
+ forwarded to `NumberFormatter#localized_number`'s `format:` keyword, taking
691
+ precedence over the configured formatter's symbols for that one element.
692
+ Custom formatters with a simpler `localized_number(value)` signature are
693
+ unaffected unless `options[:format]` is supplied, in which case their method
694
+ must accept the `format:` keyword.
695
+
587
696
 
588
697
  `decimal`:: (`String` value)
589
698
  Symbol to use for the decimal point. Accepts a character.
@@ -695,12 +804,12 @@ Missing integer digits are inserted before grouping using the configured
695
804
  "32" with `padding_group_digits: 4, group: ""` => "0032"
696
805
  "32123" with `padding_group_digits: 4, group: ""` => "00032123"
697
806
  ====
698
- ====
699
807
 
700
808
  `base`:: (`Numeric` value)
701
809
  Sets the numeric base (radix) used to render both the integer and fractional parts of the number.
702
810
  Supported values are 2 (binary), 8 (octal), 10 (decimal, default) and 16 (hexadecimal).
703
- Passing any other value for `base` raises `Plurimath::Formatter::UnsupportedBase`.
811
+ The value is also accepted as its `String` or `Symbol` equivalent (`"16"`).
812
+ Passing any other value for `base` raises `Plurimath::Errors::UnsupportedBase`.
704
813
  +
705
814
  .Rendering numbers in different bases
706
815
  [example]
@@ -752,6 +861,10 @@ uppercase letters. This includes both hexadecimal digits and any separators
752
861
  Use `:numbers_only` to uppercase generated hexadecimal digits without changing
753
862
  separator characters. It has no effect for other bases.
754
863
  +
864
+ Both values are also accepted as their `String` or `Symbol` equivalents
865
+ (`"true"`, `:true`, `"numbers_only"`). Matching is case-sensitive; any other
866
+ value (including any form of `false`) disables uppercasing.
867
+ +
755
868
  .Uppercase hexadecimal output
756
869
  [example]
757
870
  ====
@@ -775,6 +888,205 @@ The decimal separator is rendered as configured by the `decimal` option.
775
888
  NOTE: The fractional-part conversion treats the digits after the decimal point as a fractional value (for example, `"10.75"` is interpreted
776
889
  as decimal `0.75` for the fractional part) and converts that real value to the requested base, honoring the configured precision.
777
890
 
891
+
892
+ ===== Semantic base notation rendering
893
+
894
+ When numbers are formatted with a non-decimal `base`, Plurimath produces
895
+ *structured output* in each representation language rather than flat
896
+ prefix/postfix strings. The base is rendered as a subscript, making the
897
+ output semantically rich and accessible to screen readers and other tools.
898
+
899
+ For plain-text uses (`localized_number`), prefix/postfix notation is still
900
+ available through the `base_prefix` and `base_postfix` options.
901
+
902
+ .Semantic rendering by format
903
+ [example]
904
+ ====
905
+ [source,ruby]
906
+ ----
907
+ formatter = Plurimath::Formatter::Standard.new(options: { base: 16 })
908
+ Plurimath.configuration.number_formatter = formatter
909
+
910
+ formula = Plurimath::Math.parse("255", :asciimath)
911
+
912
+ formula.to_asciimath
913
+ # => "ff_(16)"
914
+
915
+ formula.to_latex
916
+ # => "\\mathrm{ff}_{16}"
917
+
918
+ formula.to_html
919
+ # => "ff<sub>16</sub>"
920
+
921
+ formula.to_unicodemath
922
+ # => "ff_(16)"
923
+ ----
924
+
925
+ MathML produces a structured `<msub>` element:
926
+
927
+ [source,xml]
928
+ ----
929
+ <msub><mn>ff</mn><mn>16</mn></msub>
930
+ ----
931
+
932
+ OMML produces a structured `<m:sSub>` element:
933
+
934
+ [source,xml]
935
+ ----
936
+ <m:sSub>
937
+ <m:sSubPr>...</m:sSubPr>
938
+ <m:e><m:r><m:t>ff</m:t></m:r></m:e>
939
+ <m:sub><m:r><m:t>16</m:t></m:r></m:sub>
940
+ </m:sSub>
941
+ ----
942
+ ====
943
+
944
+ Negative numbers wrap the sign around the subscript structure:
945
+
946
+ [source,ruby]
947
+ ----
948
+ formula = Plurimath::Math.parse("-255", :asciimath)
949
+ formula.to_latex
950
+ # => "-\\mathrm{ff}_{16}"
951
+ ----
952
+
953
+ [source,xml]
954
+ ----
955
+ <mrow><mo>-</mo><msub><mn>ff</mn><mn>16</mn></msub></mrow>
956
+ ====
957
+
958
+
959
+ ===== Structured notation rendering
960
+
961
+ Scientific and engineering notation also produces structured output in MathML.
962
+ The `× 10^exp` portion is rendered as separate elements:
963
+
964
+ [source,ruby]
965
+ ----
966
+ formatter = Plurimath::Formatter::Standard.new(
967
+ options: { notation: :scientific, exponent_sign: :plus },
968
+ )
969
+ Plurimath.configuration.number_formatter = formatter
970
+
971
+ formula = Plurimath::Math.parse("14000", :asciimath)
972
+ ----
973
+
974
+ [source,xml]
975
+ ----
976
+ <mrow>
977
+ <mn>1.4000</mn>
978
+ <mo>×</mo>
979
+ <msup><mn>10</mn><mn>+4</mn></msup>
980
+ </mrow>
981
+ ----
982
+
983
+ E-notation remains as a flat `<mn>` element:
984
+
985
+ [source,xml]
986
+ ----
987
+ <mn>1.4000E+4</mn>
988
+ ====
989
+
990
+
991
+ ===== Structured formatter API
992
+
993
+ `NumberFormatter#localized_number` continues to return a flat string for
994
+ backward compatibility. A new `NumberFormatter#formatted_number` method returns
995
+ a structured value object that carries the semantic parts of the formatted
996
+ number.
997
+
998
+ [source,ruby]
999
+ ----
1000
+ formatter = Plurimath::NumberFormatter.new(:en)
1001
+ result = formatter.formatted_number("255", format: { base: 16 })
1002
+
1003
+ result # => FormattedNumber
1004
+ result.to_s # => "0xff"
1005
+ result.digits_string # => "ff"
1006
+ result.sign_text # => nil (no sign for positive numbers)
1007
+ result.base_notation.base # => 16
1008
+ result.negative? # => false
1009
+ result.fractional? # => false
1010
+ ----
1011
+
1012
+ The `sign_text` accessor returns `"-"` for negative numbers, `"+"` when
1013
+ `number_sign: :plus` is set, and `nil` otherwise. Output renderers use it
1014
+ to produce format-specific sign elements.
1015
+
1016
+ For notation numbers, `formatted_number` returns a `FormattedNotation` value
1017
+ object:
1018
+
1019
+ [source,ruby]
1020
+ ----
1021
+ result = formatter.formatted_number("14000", format: { notation: :scientific })
1022
+
1023
+ result # => FormattedNotation
1024
+ result.to_s # => "1.4000 × 10^4"
1025
+ result.coefficient # => FormattedNumber
1026
+ result.coefficient.to_s # => "1.4000"
1027
+ result.exponent # => 4
1028
+ result.formatted_exponent # => "4" ("+4" when exponent_sign: :plus)
1029
+ result.notation_style # => :scientific
1030
+ ----
1031
+
1032
+ These value objects are what Plurimath uses internally to produce the
1033
+ structured output shown above.
1034
+
1035
+
1036
+ ===== Storing base on Math::Number
1037
+
1038
+ `Plurimath::Math::Number` now accepts a `base:` keyword, storing the radix
1039
+ as a semantic attribute. When present, the stored base is used as the default
1040
+ for formatting — no explicit `base:` option is needed in the format hash.
1041
+
1042
+ [source,ruby]
1043
+ ----
1044
+ number = Plurimath::Math::Number.new("FF", base: 16)
1045
+ # When rendered with a configured formatter, the stored base (16) is used:
1046
+ number.to_asciimath(options: {})
1047
+ # => "ff_(16)"
1048
+ ----
1049
+
1050
+ The stored base represents the *semantic truth* about the number and takes
1051
+ precedence over the formatter's built-in base configuration. An explicit
1052
+ `format: { base: N }` parameter still overrides everything.
1053
+
1054
+
1055
+ ===== Parse-time base prefix literals
1056
+
1057
+ All four input parsers (AsciiMath, LaTeX, UnicodeMath, HTML) recognize
1058
+ base-prefixed number literals and store the base automatically on the
1059
+ resulting `Math::Number` node — no manual `base:` keyword needed.
1060
+
1061
+ [cols="1,1,3", options="header"]
1062
+ |===
1063
+ |Prefix |Base |Example
1064
+
1065
+ |`0x` / `0X` |16 |`0xFF`, `0xBE EF`
1066
+ |`0b` / `0B` |2 |`0b1010`
1067
+ |`0o` / `0O` |8 |`0o17`
1068
+ |===
1069
+
1070
+ [source,ruby]
1071
+ ----
1072
+ formula = Plurimath::Math.parse("0xFF", :asciimath)
1073
+ formula.value.first.value # => "FF"
1074
+ formula.value.first.base # => 16
1075
+
1076
+ # The stored base drives semantic rendering in every output format:
1077
+ formula.to_asciimath # => "ff_(16)"
1078
+ formula.to_latex # => "\mathrm{ff}_{16}"
1079
+ formula.to_html # => "ff<sub>16</sub>"
1080
+ ----
1081
+
1082
+ NOTE: Hex literals store the original hex digits (`"FF"`) because letters
1083
+ are unambiguous. Binary and octal literals store the *decimal equivalent*
1084
+ (`0b1010` → value `"10"`) because their digit sets overlap with decimal.
1085
+
1086
+ The shared `Plurimath::BaseNumberPrefix` module defines these parser and
1087
+ transform rules once; every format parser includes it. Adding a new base
1088
+ prefix requires editing only `lib/plurimath/base_number_prefix.rb`.
1089
+
778
1090
  ===== Base conversion with other formatting options
779
1091
 
780
1092
  Base conversion works seamlessly with other formatting options such as `precision`, `digit_count`,
@@ -1027,8 +1339,11 @@ In this example it is `' '` (a space).
1027
1339
  ----
1028
1340
  formatter = Plurimath::NumberFormatter.new(:en, localize_number: "#,##0.### ###")
1029
1341
  formatter.localized_number("1234.56789")
1030
- # => "1,234.568 9"
1342
+ # => "1,234.567 89"
1031
1343
  ----
1344
+
1345
+ NOTE: A space used as a group or fraction-group delimiter in the
1346
+ `localize_number` pattern is emitted as U+00A0 NO-BREAK SPACE in the output.
1032
1347
  ====
1033
1348
 
1034
1349
 
@@ -1052,7 +1367,7 @@ formatter.localized_number(
1052
1367
  ----
1053
1368
  <1> The number to be formatted.
1054
1369
  <2> The locale to be used for number formatting.
1055
- <3> The number of decimal places to round the number to.
1370
+ <3> The number of decimal places to keep; the fraction is truncated, not rounded.
1056
1371
  <4> Hash containing the relevant options for number formatting.
1057
1372
 
1058
1373
  Where,
@@ -1065,8 +1380,8 @@ Value is a symbol.
1065
1380
  Overrides the locale set during the creation of the `NumberFormatter` object. If
1066
1381
  not provided, the locale of the `NumberFormatter` instance will be used.
1067
1382
 
1068
- `precision: <precision-number>`:: (optional) The number of decimal places to round the
1069
- number to. If not provided, the precision of the `NumberFormatter` instance will
1383
+ `precision: <precision-number>`:: (optional) The number of decimal places to keep
1384
+ (the fraction is truncated, not rounded). If not provided, the precision of the `NumberFormatter` instance will
1070
1385
  be used.
1071
1386
 
1072
1387
  `format: <format-hash>`:: (optional, default `{}`) A Hash containing the relevant
@@ -1075,7 +1390,8 @@ configuration of the `NumberFormatter`.
1075
1390
  Takes a Hash in the form of the <<localizer_symbols,format options hash>>.
1076
1391
 
1077
1392
  `precision: <precision-number>`::
1078
- Number of decimal places to round. Accepts an integer value.
1393
+ Number of decimal places to keep; the fraction is truncated, not
1394
+ rounded. Accepts an integer value.
1079
1395
  +
1080
1396
  .Specifying a precision of 6 digits
1081
1397
  [example]
@@ -1631,12 +1947,12 @@ end
1631
1947
 
1632
1948
  Formula rendering uses the first formatter method available in this order:
1633
1949
  `format_number(formula, number)`, then `format(formula, number)`, then
1634
- `localized_number(number.value.to_s)`. This keeps existing custom formatters
1635
- working while giving new custom formatters a method name specific to formula
1636
- number formatting.
1637
-
1638
- New custom formatters should define `format_number`; the `format` fallback
1639
- exists for compatibility with existing custom formatters.
1950
+ `formatted_number(number.value.to_s)` (returns a structured value object),
1951
+ then `localized_number(number.value.to_s)` (returns a flat string). This
1952
+ keeps existing custom formatters working while giving new code access to the
1953
+ structured result through `formatted_number`. New custom formatters should
1954
+ define `format_number`; the `format` fallback exists for compatibility with
1955
+ existing custom formatters.
1640
1956
 
1641
1957
  To implement contextual formatting, define a `format_number` method in a custom
1642
1958
  formatter subclass.
@@ -3,6 +3,8 @@
3
3
  module Plurimath
4
4
  class Asciimath
5
5
  class Parse < Parslet::Parser
6
+ include Plurimath::BaseNumberPrefix::Parser
7
+
6
8
  rule(:td) { expression.as(:td) }
7
9
  rule(:base) { str("__|").absent? >> str("_") }
8
10
  rule(:power) { str("^") }
@@ -10,7 +12,10 @@ module Plurimath
10
12
  rule(:comma) { str(",") >> space? }
11
13
  rule(:space?) { space.maybe }
12
14
  rule(:number) do
13
- (match("[0-9]").repeat(1) >> decimal_marker >> match("[0-9]").repeat(1)).as(:number) |
15
+ hex_number |
16
+ binary_number |
17
+ octal_number |
18
+ (match("[0-9]").repeat(1) >> decimal_marker >> match("[0-9]").repeat(1)).as(:number) |
14
19
  match("[0-9]").repeat(1).as(:number) |
15
20
  str(".").as(:symbol)
16
21
  end
@@ -3,6 +3,8 @@
3
3
  module Plurimath
4
4
  class Asciimath
5
5
  class Transform < Parslet::Transform
6
+ include Plurimath::BaseNumberPrefix::Transform
7
+
6
8
  rule(mod: simple(:mod)) { mod }
7
9
  rule(frac: simple(:frac)) { frac }
8
10
  rule(unary: simple(:unary)) { unary }
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ # Shared base-prefix number literal rules (0x, 0b, 0o) for all Parslet
5
+ # parsers and transforms. Including the Parser sub-module adds hex/binary/
6
+ # octal parser atoms; including the Transform sub-module adds the
7
+ # corresponding Math::Number construction rules. Define base prefixes here
8
+ # once — every format parser picks them up automatically.
9
+ module BaseNumberPrefix
10
+ # Adds Parslet parser rules for hex/binary/octal prefixed literals.
11
+ # Include in any Parslet::Parser subclass.
12
+ module Parser
13
+ def self.included(base)
14
+ base.class_eval do
15
+ rule(:hex_number) do
16
+ (str("0x") | str("0X")) >> match["0-9a-fA-F"].repeat(1).as(:hex_number)
17
+ end
18
+
19
+ rule(:binary_number) do
20
+ (str("0b") | str("0B")) >> match["01"].repeat(1).as(:binary_number)
21
+ end
22
+
23
+ rule(:octal_number) do
24
+ (str("0o") | str("0O")) >> match["0-7"].repeat(1).as(:octal_number)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Adds Parslet transform rules that build Math::Number with the correct
31
+ # base attribute from hex/binary/octal parse tree nodes.
32
+ # Include in any Parslet::Transform subclass.
33
+ module Transform
34
+ def self.included(base)
35
+ base.class_eval do
36
+ rule(hex_number: simple(:hex)) { Math::Number.new(hex.to_s, base: 16) }
37
+ rule(binary_number: simple(:bin)) { Math::Number.new(bin.to_s.to_i(2).to_s, base: 2) }
38
+ rule(octal_number: simple(:oct)) { Math::Number.new(oct.to_s.to_i(8).to_s, base: 8) }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -4,7 +4,15 @@ module Plurimath
4
4
  class Configuration
5
5
  DEFAULT_DECIMAL = "."
6
6
 
7
- attr_accessor :number_formatter, :locale
7
+ # Cap on bounded sum/prod iterations, guarding the untrusted-document
8
+ # evaluation path against runaway loops. Set to nil to disable the cap.
9
+ DEFAULT_MAX_ITERATIONS = 100_000
10
+
11
+ attr_accessor :number_formatter, :locale, :evaluation_max_iterations
12
+
13
+ def initialize
14
+ @evaluation_max_iterations = DEFAULT_MAX_ITERATIONS
15
+ end
8
16
 
9
17
  def deprecation
10
18
  Deprecation
@@ -6,7 +6,8 @@ module Plurimath
6
6
  DEFAULT_BEHAVIOR = :collect
7
7
 
8
8
  class << self
9
- def warn(feature:, message: nil, replacement: nil, since: nil, remove_in: nil)
9
+ def warn(feature:, message: nil, replacement: nil, since: nil,
10
+ remove_in: nil)
10
11
  feature = validate_feature(feature)
11
12
  return if behavior == :collect && emitted_features[feature]
12
13
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Plurimath
4
4
  class ConfigurationError < Error
5
- def initialize(type, value: nil, supported: nil)
5
+ def initialize(type, value: nil, supported: nil, option: nil)
6
6
  @type = type
7
7
  @value = value
8
8
  @supported = supported
9
+ @option = option
9
10
  super(message)
10
11
  end
11
12
 
@@ -19,6 +20,9 @@ module Plurimath
19
20
  when :conflicting_formatter_options
20
21
  "formatter options cannot be used together: choose either " \
21
22
  ":padding_digits or :padding_group_digits"
23
+ when :invalid_formatter_option
24
+ "invalid value #{@value.inspect} for formatter option " \
25
+ "#{@option.inspect}#{" (expected #{@supported})" if @supported}"
22
26
  else
23
27
  "invalid Plurimath configuration"
24
28
  end
@@ -6,7 +6,8 @@ module Plurimath
6
6
 
7
7
  attr_reader :feature, :replacement, :since, :remove_in
8
8
 
9
- def initialize(feature:, message: nil, replacement: nil, since: nil, remove_in: nil)
9
+ def initialize(feature:, message: nil, replacement: nil, since: nil,
10
+ remove_in: nil)
10
11
  @feature = feature
11
12
  @replacement = replacement
12
13
  @since = since
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class DivisionByZeroError < Error
7
+ def initialize
8
+ super("divided by 0")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class Error < Plurimath::Error; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class InvalidBindingError < Error
7
+ def initialize(name, value)
8
+ super("wrong value for variable `#{name}` " \
9
+ "(given #{value.class}, expected a real number)")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class InvalidBindingKeyError < Error
7
+ def initialize(key)
8
+ super("wrong type for binding key " \
9
+ "(given #{key.class}, expected String or Symbol)")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class MathDomainError < Error; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class MissingVariableError < Error
7
+ def initialize(name)
8
+ super("missing value for variable `#{name}`")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class NonFiniteResultError < Error
7
+ def initialize
8
+ super("result is not a finite number")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end