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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +311 -58
- data/Gemfile +3 -2
- data/README.adoc +331 -15
- data/lib/plurimath/asciimath/parse.rb +6 -1
- data/lib/plurimath/asciimath/transform.rb +2 -0
- data/lib/plurimath/base_number_prefix.rb +43 -0
- data/lib/plurimath/configuration.rb +9 -1
- data/lib/plurimath/deprecation.rb +2 -1
- data/lib/plurimath/errors/configuration_error.rb +5 -1
- data/lib/plurimath/errors/deprecation_error.rb +2 -1
- data/lib/plurimath/errors/evaluation/division_by_zero_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/error.rb +9 -0
- data/lib/plurimath/errors/evaluation/invalid_binding_error.rb +14 -0
- data/lib/plurimath/errors/evaluation/invalid_binding_key_error.rb +14 -0
- data/lib/plurimath/errors/evaluation/math_domain_error.rb +9 -0
- data/lib/plurimath/errors/evaluation/missing_variable_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/non_finite_result_error.rb +13 -0
- data/lib/plurimath/errors/evaluation/unsupported_expression_error.rb +13 -0
- data/lib/plurimath/errors/evaluation.rb +18 -0
- data/lib/plurimath/errors/invalid_number.rb +17 -0
- data/lib/plurimath/errors/unsupported_base.rb +18 -0
- data/lib/plurimath/errors/{formatter/unsupported_locale.rb → unsupported_locale.rb} +1 -1
- data/lib/plurimath/errors.rb +9 -0
- data/lib/plurimath/formatter/numbers/base_notation.rb +55 -27
- data/lib/plurimath/formatter/numbers/format_options.rb +123 -17
- data/lib/plurimath/formatter/numbers/formatted_notation.rb +62 -0
- data/lib/plurimath/formatter/numbers/formatted_number.rb +87 -0
- data/lib/plurimath/formatter/numbers/fraction.rb +8 -17
- data/lib/plurimath/formatter/numbers/integer.rb +3 -0
- data/lib/plurimath/formatter/numbers/mathml_renderer.rb +56 -0
- data/lib/plurimath/formatter/numbers/notation_renderer.rb +58 -28
- data/lib/plurimath/formatter/numbers/number_renderer.rb +26 -13
- data/lib/plurimath/formatter/numbers/omml_renderer.rb +74 -0
- data/lib/plurimath/formatter/numbers/precision_resolver.rb +16 -3
- data/lib/plurimath/formatter/numbers/sign_renderer.rb +2 -1
- data/lib/plurimath/formatter/numbers/significant.rb +4 -2
- data/lib/plurimath/formatter/numbers/source.rb +46 -6
- data/lib/plurimath/formatter/numbers/text_renderer.rb +52 -0
- data/lib/plurimath/formatter/numbers.rb +6 -2
- data/lib/plurimath/formatter/standard.rb +6 -0
- data/lib/plurimath/formatter/supported_locales.rb +1 -1
- data/lib/plurimath/formatter.rb +0 -2
- data/lib/plurimath/html/parse.rb +5 -0
- data/lib/plurimath/html/transform.rb +26 -12
- data/lib/plurimath/latex/parse.rb +5 -0
- data/lib/plurimath/latex/transform.rb +2 -0
- data/lib/plurimath/math/core.rb +69 -14
- data/lib/plurimath/math/evaluation/evaluator.rb +147 -0
- data/lib/plurimath/math/evaluation/expression_parser.rb +215 -0
- data/lib/plurimath/math/evaluation/iteration.rb +63 -0
- data/lib/plurimath/math/evaluation.rb +13 -0
- data/lib/plurimath/math/formula.rb +9 -0
- data/lib/plurimath/math/function/abs.rb +4 -0
- data/lib/plurimath/math/function/arccos.rb +4 -0
- data/lib/plurimath/math/function/arcsin.rb +4 -0
- data/lib/plurimath/math/function/arctan.rb +4 -0
- data/lib/plurimath/math/function/ceil.rb +4 -0
- data/lib/plurimath/math/function/cos.rb +4 -0
- data/lib/plurimath/math/function/cosh.rb +4 -0
- data/lib/plurimath/math/function/cot.rb +4 -0
- data/lib/plurimath/math/function/coth.rb +4 -0
- data/lib/plurimath/math/function/csc.rb +4 -0
- data/lib/plurimath/math/function/csch.rb +4 -0
- data/lib/plurimath/math/function/exp.rb +4 -0
- data/lib/plurimath/math/function/fenced.rb +4 -0
- data/lib/plurimath/math/function/floor.rb +4 -0
- data/lib/plurimath/math/function/frac.rb +7 -0
- data/lib/plurimath/math/function/gcd.rb +9 -0
- data/lib/plurimath/math/function/lcm.rb +9 -0
- data/lib/plurimath/math/function/lg.rb +4 -0
- data/lib/plurimath/math/function/ln.rb +4 -0
- data/lib/plurimath/math/function/log.rb +19 -0
- data/lib/plurimath/math/function/max.rb +4 -0
- data/lib/plurimath/math/function/min.rb +4 -0
- data/lib/plurimath/math/function/mod.rb +15 -0
- data/lib/plurimath/math/function/overleftrightarrow.rb +2 -2
- data/lib/plurimath/math/function/power.rb +10 -0
- data/lib/plurimath/math/function/prod.rb +10 -0
- data/lib/plurimath/math/function/root.rb +7 -0
- data/lib/plurimath/math/function/sec.rb +4 -0
- data/lib/plurimath/math/function/sech.rb +4 -0
- data/lib/plurimath/math/function/sin.rb +4 -0
- data/lib/plurimath/math/function/sinh.rb +4 -0
- data/lib/plurimath/math/function/sqrt.rb +4 -0
- data/lib/plurimath/math/function/sum.rb +10 -0
- data/lib/plurimath/math/function/tan.rb +4 -0
- data/lib/plurimath/math/function/tanh.rb +4 -0
- data/lib/plurimath/math/function/text.rb +17 -0
- data/lib/plurimath/math/number.rb +44 -23
- data/lib/plurimath/math/symbols/cdot.rb +4 -0
- data/lib/plurimath/math/symbols/div.rb +4 -0
- data/lib/plurimath/math/symbols/hat.rb +4 -0
- data/lib/plurimath/math/symbols/minus.rb +4 -0
- data/lib/plurimath/math/symbols/pi.rb +5 -1
- data/lib/plurimath/math/symbols/plus.rb +4 -0
- data/lib/plurimath/math/symbols/slash.rb +4 -0
- data/lib/plurimath/math/symbols/symbol.rb +45 -0
- data/lib/plurimath/math/symbols/times.rb +4 -0
- data/lib/plurimath/math.rb +5 -1
- data/lib/plurimath/mathml/constants.rb +18 -0
- data/lib/plurimath/number_formatter.rb +63 -3
- data/lib/plurimath/omml/formula_transformation.rb +2 -1
- data/lib/plurimath/omml/translator.rb +4 -1
- data/lib/plurimath/setup/opal.rb.erb +13 -0
- data/lib/plurimath/unicode_math/parse.rb +98 -54
- data/lib/plurimath/unicode_math/parser.rb +7 -4
- data/lib/plurimath/unicode_math/parsing_rules/absence_rules.rb +33 -21
- data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +28 -16
- data/lib/plurimath/unicode_math/parsing_rules/constants_rules.rb +18 -7
- data/lib/plurimath/unicode_math/parsing_rules/masked.rb +35 -15
- data/lib/plurimath/unicode_math/parsing_rules/sub_sup.rb +98 -72
- data/lib/plurimath/unicode_math/transform.rb +1 -0
- data/lib/plurimath/utility.rb +1 -1
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +2 -0
- metadata +24 -5
- data/lib/plurimath/errors/formatter/unsupported_base.rb +0 -21
- data/lib/plurimath/formatter/numbers/parts_renderer.rb +0 -30
|
@@ -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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
class InvalidNumber < Plurimath::Error
|
|
6
|
+
def initialize(value)
|
|
7
|
+
@value = value
|
|
8
|
+
super(message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"[plurimath] Invalid number #{@value.inspect} for number formatting. " \
|
|
13
|
+
"Expected a numeric string such as \"1234\", \"-12.34\", or \"1.2e5\"."
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
class UnsupportedBase < Plurimath::Error
|
|
6
|
+
def initialize(base, supported_bases)
|
|
7
|
+
@base = base
|
|
8
|
+
@supported = supported_bases.keys.join(", ")
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def message
|
|
13
|
+
"[plurimath] Unsupported base `#{@base}` for number formatting. " \
|
|
14
|
+
"[plurimath] The formatter `:base` option must be one of: #{@supported}."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/plurimath/errors.rb
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
autoload :Evaluation, "#{__dir__}/errors/evaluation"
|
|
6
|
+
autoload :InvalidNumber, "#{__dir__}/errors/invalid_number"
|
|
7
|
+
autoload :UnsupportedBase, "#{__dir__}/errors/unsupported_base"
|
|
8
|
+
autoload :UnsupportedLocale, "#{__dir__}/errors/unsupported_locale"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
module Numbers
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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,54 +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(
|
|
19
|
-
|
|
20
|
-
@base =
|
|
21
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
def self.from_options(options)
|
|
31
|
+
base = options.base
|
|
32
|
+
validate!(base)
|
|
27
33
|
|
|
28
|
-
|
|
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)
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
def default?
|
|
32
49
|
base == Base::DEFAULT_BASE
|
|
33
50
|
end
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
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}"
|
|
37
69
|
end
|
|
38
70
|
|
|
39
71
|
private
|
|
40
72
|
|
|
41
|
-
attr_reader :
|
|
73
|
+
attr_reader :explicit_prefix, :explicit_postfix
|
|
42
74
|
|
|
43
|
-
|
|
75
|
+
alias explicit_prefix? explicit_prefix
|
|
76
|
+
alias explicit_postfix? explicit_postfix
|
|
77
|
+
|
|
78
|
+
def self.resolve_prefix(options, base)
|
|
44
79
|
return options.base_prefix if options.base_prefix?
|
|
45
|
-
# A postfix without an explicit prefix opts out of the default prefix.
|
|
46
80
|
return "" if options.base_postfix?
|
|
47
81
|
|
|
48
82
|
DEFAULT_PREFIXES[base]
|
|
49
83
|
end
|
|
84
|
+
private_class_method :resolve_prefix
|
|
50
85
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def upcase_hex?
|
|
56
|
-
base == 16 && options.hex_capital == true
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def validate_base!
|
|
60
|
-
return if self.class.supported?(base)
|
|
86
|
+
def self.validate!(base)
|
|
87
|
+
return if supported?(base)
|
|
61
88
|
|
|
62
|
-
raise UnsupportedBase.new(base, DEFAULT_PREFIXES)
|
|
89
|
+
raise Plurimath::Errors::UnsupportedBase.new(base, DEFAULT_PREFIXES)
|
|
63
90
|
end
|
|
91
|
+
private_class_method :validate!
|
|
64
92
|
end
|
|
65
93
|
end
|
|
66
94
|
end
|
|
@@ -32,11 +32,16 @@ module Plurimath
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def base
|
|
35
|
-
symbols[:base]
|
|
35
|
+
value = symbols[:base]
|
|
36
|
+
return Base::DEFAULT_BASE if value.nil?
|
|
37
|
+
|
|
38
|
+
# Numeric String/Symbol forms are normalized; anything else is left
|
|
39
|
+
# raw so BaseNotation reports it through UnsupportedBase.
|
|
40
|
+
coerce_integer(value) { return value }
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def base_prefix
|
|
39
|
-
|
|
44
|
+
separator_option(:base_prefix).to_s
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def base_prefix?
|
|
@@ -44,7 +49,7 @@ module Plurimath
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def base_postfix
|
|
47
|
-
|
|
52
|
+
separator_option(:base_postfix)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def base_postfix?
|
|
@@ -52,56 +57,84 @@ module Plurimath
|
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
def decimal
|
|
55
|
-
|
|
60
|
+
# An explicitly passed nil renders without a separator; output
|
|
61
|
+
# correctness is then the caller's responsibility.
|
|
62
|
+
separator_option(:decimal, default: DEFAULT_DECIMAL)
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def digit_count
|
|
59
|
-
|
|
66
|
+
integer_option(:digit_count, default: 0)
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
def fraction_group
|
|
63
|
-
|
|
70
|
+
separator_option(:fraction_group).to_s
|
|
64
71
|
end
|
|
65
72
|
|
|
66
73
|
def fraction_group_digits
|
|
67
|
-
|
|
74
|
+
integer_option(:fraction_group_digits)
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def group
|
|
71
|
-
|
|
78
|
+
# Unlike decimal, an explicit nil group falls back to the default
|
|
79
|
+
# separator; use "" to disable grouping.
|
|
80
|
+
(separator_option(:group,
|
|
81
|
+
default: DEFAULT_GROUP) || DEFAULT_GROUP).to_s
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
def group_digits
|
|
75
|
-
|
|
85
|
+
integer_option(:group_digits, default: DEFAULT_GROUP_DIGITS)
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
def hex_capital
|
|
79
|
-
symbols[:hex_capital]
|
|
89
|
+
value = symbols[:hex_capital]
|
|
90
|
+
# nil is handled first: under Opal `when nil` spuriously matches
|
|
91
|
+
# non-nil values (nil === 1.5 is true there), so it must not appear
|
|
92
|
+
# in the type whitelist below.
|
|
93
|
+
return if value.nil?
|
|
94
|
+
|
|
95
|
+
# Accept only Boolean/String/Symbol and reject any other type.
|
|
96
|
+
# Attribute-parsing callers deliver String/Symbol (:true,
|
|
97
|
+
# "numbers_only"), so the accepted forms normalize through to_s.
|
|
98
|
+
case value
|
|
99
|
+
when true, false, String, Symbol
|
|
100
|
+
case value.to_s
|
|
101
|
+
when "true" then true
|
|
102
|
+
when "numbers_only" then :numbers_only
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
invalid_option!(:hex_capital, value)
|
|
106
|
+
end
|
|
80
107
|
end
|
|
81
108
|
|
|
82
109
|
def number_sign
|
|
83
|
-
|
|
110
|
+
symbol_option(:number_sign)
|
|
84
111
|
end
|
|
85
112
|
|
|
86
113
|
def padding
|
|
87
|
-
value =
|
|
114
|
+
value = separator_option(:padding, default: DEFAULT_PADDING).to_s
|
|
88
115
|
value.empty? ? DEFAULT_PADDING : value[0]
|
|
89
116
|
end
|
|
90
117
|
|
|
91
118
|
def padding_digits
|
|
92
|
-
|
|
119
|
+
integer_option(:padding_digits, default: 0)
|
|
93
120
|
end
|
|
94
121
|
|
|
95
122
|
def padding_group_digits
|
|
96
|
-
|
|
123
|
+
integer_option(:padding_group_digits, default: 0)
|
|
97
124
|
end
|
|
98
125
|
|
|
99
126
|
def notation_supported?
|
|
100
127
|
NotationRenderer.supported?(notation)
|
|
101
128
|
end
|
|
102
129
|
|
|
130
|
+
# Whether precision came from the caller (kwarg or symbols) rather
|
|
131
|
+
# than being inferred from the source by the resolver.
|
|
132
|
+
def explicit_precision?
|
|
133
|
+
@explicit_precision
|
|
134
|
+
end
|
|
135
|
+
|
|
103
136
|
def significant
|
|
104
|
-
|
|
137
|
+
integer_option(:significant, default: 0)
|
|
105
138
|
end
|
|
106
139
|
|
|
107
140
|
def to_h
|
|
@@ -111,7 +144,19 @@ module Plurimath
|
|
|
111
144
|
private
|
|
112
145
|
|
|
113
146
|
def resolve_precision(source, precision, precision_resolver)
|
|
114
|
-
|
|
147
|
+
# A nil check (not ||) so that explicit false reaches validation.
|
|
148
|
+
effective_precision = precision.nil? ? symbols[:precision] : precision
|
|
149
|
+
unless effective_precision.nil?
|
|
150
|
+
value = effective_precision
|
|
151
|
+
effective_precision = coerce_integer(value) do
|
|
152
|
+
invalid_option!(:precision, value)
|
|
153
|
+
end
|
|
154
|
+
if effective_precision.negative?
|
|
155
|
+
invalid_option!(:precision, value,
|
|
156
|
+
expected: "a non-negative integer")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
@explicit_precision = !effective_precision.nil?
|
|
115
160
|
return effective_precision unless precision_resolver
|
|
116
161
|
|
|
117
162
|
precision_resolver.resolve(
|
|
@@ -119,12 +164,73 @@ module Plurimath
|
|
|
119
164
|
precision: effective_precision,
|
|
120
165
|
base: base,
|
|
121
166
|
significant: significant,
|
|
167
|
+
digit_count: digit_count,
|
|
122
168
|
notation_supported: notation_supported?,
|
|
123
169
|
)
|
|
124
170
|
end
|
|
125
171
|
|
|
172
|
+
# Counts must be non-negative Integers; numeric String/Symbol forms are
|
|
173
|
+
# normalized because attribute-parsing callers deliver those.
|
|
174
|
+
def integer_option(key, default: nil)
|
|
175
|
+
value = symbols[key]
|
|
176
|
+
return default if value.nil?
|
|
177
|
+
|
|
178
|
+
integer = coerce_integer(value) { invalid_option!(key, value) }
|
|
179
|
+
if integer.negative?
|
|
180
|
+
invalid_option!(key, value,
|
|
181
|
+
expected: "a non-negative integer")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
integer
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def coerce_integer(value)
|
|
188
|
+
# Note on Opal: JS has a single number type, so a whole-valued Float
|
|
189
|
+
# (4.0) is indistinguishable from the Integer 4 (same value, same
|
|
190
|
+
# to_s "4") — under Opal it is treated as that integer. MRI rejects
|
|
191
|
+
# Floats here; non-whole Floats (1.5) are rejected in both runtimes.
|
|
192
|
+
case value
|
|
193
|
+
when ::Integer then value
|
|
194
|
+
when true, false, Float then yield
|
|
195
|
+
else
|
|
196
|
+
begin
|
|
197
|
+
Integer(value.to_s, 10)
|
|
198
|
+
rescue ArgumentError, TypeError
|
|
199
|
+
yield
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Separators are free-form strings; explicit nil disables the
|
|
205
|
+
# separator, and Booleans are always a caller bug.
|
|
206
|
+
def separator_option(key, default: nil)
|
|
207
|
+
value = symbols.fetch(key, default)
|
|
208
|
+
invalid_option!(key, value) if [true, false].include?(value)
|
|
209
|
+
|
|
210
|
+
value
|
|
211
|
+
end
|
|
212
|
+
|
|
126
213
|
def symbol_option(key)
|
|
127
|
-
symbols[key]
|
|
214
|
+
value = symbols[key]
|
|
215
|
+
return if value.nil?
|
|
216
|
+
|
|
217
|
+
# Enumerated/symbol options accept only String or Symbol; reject
|
|
218
|
+
# other types (and booleans) instead of coercing arbitrary input.
|
|
219
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
220
|
+
invalid_option!(key,
|
|
221
|
+
value)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
value.to_sym
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def invalid_option!(key, value, expected: nil)
|
|
228
|
+
raise Plurimath::ConfigurationError.new(
|
|
229
|
+
:invalid_formatter_option,
|
|
230
|
+
option: key,
|
|
231
|
+
value: value,
|
|
232
|
+
supported: expected,
|
|
233
|
+
)
|
|
128
234
|
end
|
|
129
235
|
|
|
130
236
|
def validate_padding_options!
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Structured result of notation formatting (e, scientific, engineering).
|
|
7
|
+
# Carries the coefficient as a FormattedNumber, the exponent as an
|
|
8
|
+
# integer, and notation style metadata so output renderers (MathML,
|
|
9
|
+
# LaTeX, etc.) can produce structured representations instead of flat
|
|
10
|
+
# strings.
|
|
11
|
+
class FormattedNotation
|
|
12
|
+
STYLES = %i[e scientific engineering].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :coefficient, :notation_style, :exponent,
|
|
15
|
+
:times_symbol, :exponent_separator, :exponent_sign
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
coefficient:,
|
|
19
|
+
notation_style:,
|
|
20
|
+
exponent:,
|
|
21
|
+
times_symbol: "×",
|
|
22
|
+
exponent_separator: "e",
|
|
23
|
+
exponent_sign: nil
|
|
24
|
+
)
|
|
25
|
+
@coefficient = coefficient
|
|
26
|
+
@notation_style = notation_style
|
|
27
|
+
@exponent = exponent
|
|
28
|
+
@times_symbol = times_symbol
|
|
29
|
+
@exponent_separator = exponent_separator
|
|
30
|
+
@exponent_sign = exponent_sign
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
case notation_style
|
|
35
|
+
when :e
|
|
36
|
+
"#{coefficient}#{exponent_separator}#{formatted_exponent}"
|
|
37
|
+
when :scientific, :engineering
|
|
38
|
+
"#{coefficient} #{times_symbol} 10^#{formatted_exponent}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_str
|
|
43
|
+
to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def base_notation?
|
|
47
|
+
coefficient.base_notation?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The exponent rendered as a string, applying sign conventions.
|
|
51
|
+
# Public so that output renderers can access it directly.
|
|
52
|
+
def formatted_exponent
|
|
53
|
+
return "0" if exponent.zero?
|
|
54
|
+
|
|
55
|
+
sign_prefix = exponent_sign == :plus ? "+" : nil
|
|
56
|
+
abs_exp = exponent.abs.to_s
|
|
57
|
+
exponent.negative? ? "-#{abs_exp}" : "#{sign_prefix}#{abs_exp}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
# Structured result of number formatting. Carries sign, digit parts,
|
|
7
|
+
# decimal separator, and base notation as separate semantic elements
|
|
8
|
+
# so output renderers (MathML, LaTeX, etc.) can produce structured
|
|
9
|
+
# representations instead of flat strings.
|
|
10
|
+
class FormattedNumber
|
|
11
|
+
attr_reader :sign, :integer_part, :fraction_part,
|
|
12
|
+
:decimal_separator, :base_notation, :number_sign
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
sign:,
|
|
16
|
+
integer_part:,
|
|
17
|
+
fraction_part:,
|
|
18
|
+
decimal_separator:,
|
|
19
|
+
base_notation:,
|
|
20
|
+
number_sign: nil
|
|
21
|
+
)
|
|
22
|
+
@sign = sign
|
|
23
|
+
@integer_part = integer_part
|
|
24
|
+
@fraction_part = fraction_part
|
|
25
|
+
@decimal_separator = decimal_separator
|
|
26
|
+
@base_notation = base_notation
|
|
27
|
+
@number_sign = number_sign
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negative?
|
|
31
|
+
sign == -1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fractional?
|
|
35
|
+
!fraction_part.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_notation?
|
|
39
|
+
!base_notation.default?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The sign as a rendering prefix: "-" for negative, "+" when
|
|
43
|
+
# number_sign is :plus, nil otherwise. Output renderers use this
|
|
44
|
+
# to produce format-specific sign elements.
|
|
45
|
+
def sign_text
|
|
46
|
+
if negative?
|
|
47
|
+
"-"
|
|
48
|
+
elsif number_sign == :plus
|
|
49
|
+
"+"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
digits = formatted_digits
|
|
55
|
+
digits = base_notation.wrap(digits) unless base_notation.default?
|
|
56
|
+
"#{sign_text}#{digits}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_str
|
|
60
|
+
to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Digits with decimal separator and optional hex capitalization,
|
|
64
|
+
# but without sign, prefix, or postfix. Used by structured renderers
|
|
65
|
+
# that handle sign and base notation as separate elements.
|
|
66
|
+
def digits_string
|
|
67
|
+
formatted_digits
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def formatted_digits
|
|
73
|
+
digits = assembled_digits
|
|
74
|
+
base_notation.upcase_hex? ? upcase_hex(digits) : digits
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def assembled_digits
|
|
78
|
+
fractional? ? "#{integer_part}#{decimal_separator}#{fraction_part}" : integer_part
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def upcase_hex(string)
|
|
82
|
+
string.tr(Base::HEX_DIGITS, Base::HEX_DIGITS.upcase)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -20,7 +20,7 @@ module Plurimath
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Keep fraction preparation on structured parts; localized rendering and
|
|
23
|
-
# grouping happen later at the
|
|
23
|
+
# grouping happen later at the FormattedNumber boundary.
|
|
24
24
|
def apply_parts(parts, precision: self.precision)
|
|
25
25
|
precision = precision.to_i
|
|
26
26
|
return parts.with_digits(fraction_digits: DEFAULT_STRINGS[:empty]) unless precision.positive?
|
|
@@ -28,9 +28,12 @@ module Plurimath
|
|
|
28
28
|
@integer_digits = parts.integer_digits
|
|
29
29
|
|
|
30
30
|
fraction = parts.fraction_digits
|
|
31
|
-
|
|
31
|
+
if !base_default? && fraction.match?(/[1-9]/)
|
|
32
|
+
fraction = change_base(fraction,
|
|
33
|
+
precision)
|
|
34
|
+
end
|
|
32
35
|
number = if @digit_count.positive?
|
|
33
|
-
digit_count_format(fraction
|
|
36
|
+
digit_count_format(fraction)
|
|
34
37
|
else
|
|
35
38
|
format(fraction, precision)
|
|
36
39
|
end
|
|
@@ -67,7 +70,7 @@ module Plurimath
|
|
|
67
70
|
|
|
68
71
|
# The digit_count option is a total visible-digit budget, so fraction
|
|
69
72
|
# rounding can carry back into the integer digits.
|
|
70
|
-
def digit_count_format(fraction
|
|
73
|
+
def digit_count_format(fraction)
|
|
71
74
|
integer = integer_digits + DEFAULT_STRINGS[:dot] + fraction
|
|
72
75
|
int_length = integer.length.pred # integer length; excluding the decimal point
|
|
73
76
|
if int_length > @digit_count
|
|
@@ -79,24 +82,12 @@ module Plurimath
|
|
|
79
82
|
end
|
|
80
83
|
round_base_string(fraction)
|
|
81
84
|
elsif int_length < @digit_count
|
|
82
|
-
fraction + (DEFAULT_STRINGS[:zero] * (
|
|
85
|
+
fraction + (DEFAULT_STRINGS[:zero] * (@digit_count - int_length))
|
|
83
86
|
else
|
|
84
87
|
fraction
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
90
|
|
|
88
|
-
def update_digit_count(number, precision)
|
|
89
|
-
return @digit_count unless zeros_count_in(number) == precision
|
|
90
|
-
|
|
91
|
-
@digit_count - precision + 1
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def zeros_count_in(number)
|
|
95
|
-
return unless number.chars.all?(DEFAULT_STRINGS[:zero])
|
|
96
|
-
|
|
97
|
-
number.length
|
|
98
|
-
end
|
|
99
|
-
|
|
100
91
|
def round_base_string(fraction)
|
|
101
92
|
digits = fraction[0..frac_digit_count].chars
|
|
102
93
|
discard_char = digits.pop
|
|
@@ -24,6 +24,9 @@ module Plurimath
|
|
|
24
24
|
def format_groups(string)
|
|
25
25
|
string = capitalize_hex_digits(string)
|
|
26
26
|
string = pad_integer(string)
|
|
27
|
+
# group_digits: 0 disables grouping, mirroring fraction grouping.
|
|
28
|
+
return string unless groups.positive?
|
|
29
|
+
|
|
27
30
|
tokens = []
|
|
28
31
|
|
|
29
32
|
until string.empty?
|