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
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
@@ -811,6 +888,205 @@ The decimal separator is rendered as configured by the `decimal` option.
811
888
  NOTE: The fractional-part conversion treats the digits after the decimal point as a fractional value (for example, `"10.75"` is interpreted
812
889
  as decimal `0.75` for the fractional part) and converts that real value to the requested base, honoring the configured precision.
813
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
+
814
1090
  ===== Base conversion with other formatting options
815
1091
 
816
1092
  Base conversion works seamlessly with other formatting options such as `precision`, `digit_count`,
@@ -1671,12 +1947,12 @@ end
1671
1947
 
1672
1948
  Formula rendering uses the first formatter method available in this order:
1673
1949
  `format_number(formula, number)`, then `format(formula, number)`, then
1674
- `localized_number(number.value.to_s)`. This keeps existing custom formatters
1675
- working while giving new custom formatters a method name specific to formula
1676
- number formatting.
1677
-
1678
- New custom formatters should define `format_number`; the `format` fallback
1679
- 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.
1680
1956
 
1681
1957
  To implement contextual formatting, define a `format_number` method in a custom
1682
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
@@ -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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ class UnsupportedExpressionError < Error
7
+ def initialize(message)
8
+ super("unsupported expression: #{message}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ module Evaluation
6
+ autoload :Error, "#{__dir__}/evaluation/error"
7
+ autoload :DivisionByZeroError, "#{__dir__}/evaluation/division_by_zero_error"
8
+ autoload :InvalidBindingError, "#{__dir__}/evaluation/invalid_binding_error"
9
+ autoload :InvalidBindingKeyError,
10
+ "#{__dir__}/evaluation/invalid_binding_key_error"
11
+ autoload :MathDomainError, "#{__dir__}/evaluation/math_domain_error"
12
+ autoload :MissingVariableError, "#{__dir__}/evaluation/missing_variable_error"
13
+ autoload :NonFiniteResultError, "#{__dir__}/evaluation/non_finite_result_error"
14
+ autoload :UnsupportedExpressionError,
15
+ "#{__dir__}/evaluation/unsupported_expression_error"
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Plurimath
4
4
  module Errors
5
+ autoload :Evaluation, "#{__dir__}/errors/evaluation"
5
6
  autoload :InvalidNumber, "#{__dir__}/errors/invalid_number"
6
7
  autoload :UnsupportedBase, "#{__dir__}/errors/unsupported_base"
7
8
  autoload :UnsupportedLocale, "#{__dir__}/errors/unsupported_locale"
@@ -3,8 +3,10 @@
3
3
  module Plurimath
4
4
  module Formatter
5
5
  module Numbers
6
- # Applies base prefix/postfix notation after numeric digits have already
7
- # been rendered.
6
+ # Describes base prefix/postfix notation as a resolved value object.
7
+ # Constructed from FormatOptions via .from_options; carries the resolved
8
+ # prefix and postfix strings so renderers can decide how to represent the
9
+ # base (inline text, MathML subscript, LaTeX subscript, etc.).
8
10
  class BaseNotation
9
11
  DEFAULT_PREFIXES = {
10
12
  2 => "0b",
@@ -13,59 +15,80 @@ module Plurimath
13
15
  16 => "0x",
14
16
  }.freeze
15
17
 
16
- attr_reader :base
18
+ attr_reader :base, :prefix, :postfix, :hex_capital
17
19
 
18
- def initialize(options)
19
- @options = options
20
- @base = @options.base
21
- validate_base!
20
+ def initialize(base:, prefix: "", postfix: "", hex_capital: nil,
21
+ explicit_prefix: false, explicit_postfix: false)
22
+ @base = base
23
+ @prefix = prefix
24
+ @postfix = postfix
25
+ @hex_capital = hex_capital
26
+ @explicit_prefix = explicit_prefix
27
+ @explicit_postfix = explicit_postfix
22
28
  end
23
29
 
24
- def apply(string)
25
- rendered = if upcase_hex?
26
- string.tr(Base::HEX_DIGITS,
27
- Base::HEX_DIGITS.upcase)
28
- else
29
- string
30
- end
31
- return rendered if default?
30
+ def self.from_options(options)
31
+ base = options.base
32
+ validate!(base)
32
33
 
33
- "#{base_prefix}#{rendered}#{base_postfix}"
34
+ new(
35
+ base: base,
36
+ prefix: resolve_prefix(options, base),
37
+ postfix: options.base_postfix.to_s,
38
+ hex_capital: options.hex_capital,
39
+ explicit_prefix: options.base_prefix?,
40
+ explicit_postfix: options.base_postfix?,
41
+ )
42
+ end
43
+
44
+ def self.supported?(base)
45
+ DEFAULT_PREFIXES.key?(base)
34
46
  end
35
47
 
36
48
  def default?
37
49
  base == Base::DEFAULT_BASE
38
50
  end
39
51
 
40
- def self.supported?(base)
41
- DEFAULT_PREFIXES.key?(base)
52
+ # Caller overrode prefix or postfix at format-call time. Renderers that
53
+ # honor semantic base notation (msub, \mathrm{}_{base}, _(base)) must
54
+ # defer to the literal prefix/postfix in that case.
55
+ def literal?
56
+ !default? && (explicit_prefix? || explicit_postfix?)
57
+ end
58
+
59
+ def semantic?
60
+ !default? && !literal?
61
+ end
62
+
63
+ def upcase_hex?
64
+ base == 16 && hex_capital == true
65
+ end
66
+
67
+ def wrap(digits)
68
+ "#{prefix}#{digits}#{postfix}"
42
69
  end
43
70
 
44
71
  private
45
72
 
46
- attr_reader :options
73
+ attr_reader :explicit_prefix, :explicit_postfix
47
74
 
48
- def base_prefix
75
+ alias explicit_prefix? explicit_prefix
76
+ alias explicit_postfix? explicit_postfix
77
+
78
+ def self.resolve_prefix(options, base)
49
79
  return options.base_prefix if options.base_prefix?
50
- # A postfix without an explicit prefix opts out of the default prefix.
51
80
  return "" if options.base_postfix?
52
81
 
53
82
  DEFAULT_PREFIXES[base]
54
83
  end
84
+ private_class_method :resolve_prefix
55
85
 
56
- def base_postfix
57
- options.base_postfix.to_s
58
- end
59
-
60
- def upcase_hex?
61
- base == 16 && options.hex_capital == true
62
- end
63
-
64
- def validate_base!
65
- return if self.class.supported?(base)
86
+ def self.validate!(base)
87
+ return if supported?(base)
66
88
 
67
89
  raise Plurimath::Errors::UnsupportedBase.new(base, DEFAULT_PREFIXES)
68
90
  end
91
+ private_class_method :validate!
69
92
  end
70
93
  end
71
94
  end