plurimath 0.11.11 → 0.11.13
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/.github/workflows/dependent-tests.yml +7 -0
- data/.github/workflows/shared-testsuite.yml +24 -0
- data/.gitmodules +3 -0
- data/.rspec-opal +3 -0
- data/Gemfile +18 -0
- data/README.adoc +297 -3
- data/docs/catalog.adoc +73 -0
- data/docs/uncertainty-asciimath-tree.md +92 -0
- data/docs/uncertainty.adoc +522 -0
- data/lib/plurimath/asciimath/parse.rb +43 -226
- data/lib/plurimath/asciimath/parser.rb +58 -1
- data/lib/plurimath/asciimath/slice_adapter.rb +138 -0
- data/lib/plurimath/asciimath/transform.rb +218 -0
- data/lib/plurimath/asciimath.rb +1 -0
- data/lib/plurimath/catalog/entry.rb +109 -0
- data/lib/plurimath/catalog/validation.rb +109 -0
- data/lib/plurimath/catalog.rb +96 -0
- data/lib/plurimath/cli/convert.rb +40 -0
- data/lib/plurimath/cli/helpers.rb +49 -0
- data/lib/plurimath/cli/render.rb +64 -0
- data/lib/plurimath/cli.rb +55 -57
- data/lib/plurimath/documentation.rb +99 -0
- data/lib/plurimath/errors/ambiguous_decimal_error.rb +19 -0
- data/lib/plurimath/errors/invalid_uncertainty.rb +18 -0
- data/lib/plurimath/errors/missing_dependency_error.rb +23 -0
- data/lib/plurimath/errors/rendering_failed.rb +17 -0
- data/lib/plurimath/errors/rendering_unavailable.rb +25 -0
- data/lib/plurimath/errors/unsupported_render_format.rb +18 -0
- data/lib/plurimath/errors/unsupported_semantic_tree_error.rb +12 -0
- data/lib/plurimath/errors.rb +5 -0
- data/lib/plurimath/math/evaluation/evaluator.rb +48 -0
- data/lib/plurimath/math/evaluation/expression_parser.rb +29 -20
- data/lib/plurimath/math/formula/class_tree.rb +92 -0
- data/lib/plurimath/math/formula.rb +43 -2
- data/lib/plurimath/math/function/abs.rb +6 -0
- data/lib/plurimath/math/function/arccos.rb +6 -0
- data/lib/plurimath/math/function/arcsin.rb +6 -0
- data/lib/plurimath/math/function/arctan.rb +6 -0
- data/lib/plurimath/math/function/arg.rb +20 -0
- data/lib/plurimath/math/function/bar.rb +6 -0
- data/lib/plurimath/math/function/base.rb +6 -0
- data/lib/plurimath/math/function/binary_function.rb +4 -0
- data/lib/plurimath/math/function/cancel.rb +6 -0
- data/lib/plurimath/math/function/ceil.rb +6 -0
- data/lib/plurimath/math/function/color.rb +12 -0
- data/lib/plurimath/math/function/cos.rb +6 -0
- data/lib/plurimath/math/function/cosh.rb +6 -0
- data/lib/plurimath/math/function/cot.rb +6 -0
- data/lib/plurimath/math/function/coth.rb +6 -0
- data/lib/plurimath/math/function/csc.rb +6 -0
- data/lib/plurimath/math/function/csch.rb +6 -0
- data/lib/plurimath/math/function/ddot.rb +6 -0
- data/lib/plurimath/math/function/deg.rb +6 -0
- data/lib/plurimath/math/function/det.rb +6 -0
- data/lib/plurimath/math/function/dim.rb +6 -0
- data/lib/plurimath/math/function/dot.rb +6 -0
- data/lib/plurimath/math/function/exp.rb +6 -0
- data/lib/plurimath/math/function/fenced.rb +6 -0
- data/lib/plurimath/math/function/floor.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-fraktur.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-sans-serif.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-script.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold.rb +6 -0
- data/lib/plurimath/math/function/font_style/double_struck.rb +6 -0
- data/lib/plurimath/math/function/font_style/fraktur.rb +6 -0
- data/lib/plurimath/math/function/font_style/italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/monospace.rb +6 -0
- data/lib/plurimath/math/function/font_style/normal.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif-bold-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif.rb +6 -0
- data/lib/plurimath/math/function/font_style/script.rb +6 -0
- data/lib/plurimath/math/function/font_style.rb +4 -0
- data/lib/plurimath/math/function/frac.rb +6 -0
- data/lib/plurimath/math/function/gcd.rb +6 -0
- data/lib/plurimath/math/function/glb.rb +12 -0
- data/lib/plurimath/math/function/hat.rb +6 -0
- data/lib/plurimath/math/function/hom.rb +6 -0
- data/lib/plurimath/math/function/inf.rb +6 -0
- data/lib/plurimath/math/function/int.rb +7 -1
- data/lib/plurimath/math/function/intent.rb +35 -0
- data/lib/plurimath/math/function/ker.rb +6 -0
- data/lib/plurimath/math/function/lcm.rb +12 -0
- data/lib/plurimath/math/function/lg.rb +6 -0
- data/lib/plurimath/math/function/lim.rb +6 -0
- data/lib/plurimath/math/function/liminf.rb +6 -0
- data/lib/plurimath/math/function/limits.rb +6 -0
- data/lib/plurimath/math/function/limsup.rb +6 -0
- data/lib/plurimath/math/function/ln.rb +6 -0
- data/lib/plurimath/math/function/log.rb +6 -0
- data/lib/plurimath/math/function/longdiv.rb +6 -0
- data/lib/plurimath/math/function/lub.rb +6 -0
- data/lib/plurimath/math/function/max.rb +6 -0
- data/lib/plurimath/math/function/mbox.rb +6 -0
- data/lib/plurimath/math/function/menclose.rb +19 -0
- data/lib/plurimath/math/function/merror.rb +22 -0
- data/lib/plurimath/math/function/min.rb +6 -0
- data/lib/plurimath/math/function/mlabeledtr.rb +23 -0
- data/lib/plurimath/math/function/mod.rb +6 -0
- data/lib/plurimath/math/function/msgroup.rb +20 -0
- data/lib/plurimath/math/function/multiscript.rb +6 -0
- data/lib/plurimath/math/function/nary.rb +11 -0
- data/lib/plurimath/math/function/norm.rb +6 -0
- data/lib/plurimath/math/function/obrace.rb +6 -0
- data/lib/plurimath/math/function/oint.rb +7 -1
- data/lib/plurimath/math/function/over.rb +6 -0
- data/lib/plurimath/math/function/overleftrightarrow.rb +6 -0
- data/lib/plurimath/math/function/overset.rb +6 -0
- data/lib/plurimath/math/function/phantom.rb +6 -0
- data/lib/plurimath/math/function/power.rb +6 -0
- data/lib/plurimath/math/function/power_base.rb +6 -0
- data/lib/plurimath/math/function/prod.rb +7 -1
- data/lib/plurimath/math/function/right.rb +1 -1
- data/lib/plurimath/math/function/root.rb +6 -0
- data/lib/plurimath/math/function/rule.rb +20 -0
- data/lib/plurimath/math/function/scarries.rb +20 -0
- data/lib/plurimath/math/function/sec.rb +6 -0
- data/lib/plurimath/math/function/sech.rb +6 -0
- data/lib/plurimath/math/function/semantics.rb +20 -0
- data/lib/plurimath/math/function/sin.rb +6 -0
- data/lib/plurimath/math/function/sinh.rb +6 -0
- data/lib/plurimath/math/function/sqrt.rb +6 -0
- data/lib/plurimath/math/function/stackrel.rb +6 -0
- data/lib/plurimath/math/function/substack.rb +6 -0
- data/lib/plurimath/math/function/sum.rb +7 -1
- data/lib/plurimath/math/function/sup.rb +6 -0
- data/lib/plurimath/math/function/table/align.rb +6 -0
- data/lib/plurimath/math/function/table/array.rb +6 -0
- data/lib/plurimath/math/function/table/bmatrix.rb +6 -0
- data/lib/plurimath/math/function/table/cases.rb +6 -0
- data/lib/plurimath/math/function/table/eqarray.rb +6 -0
- data/lib/plurimath/math/function/table/matrix.rb +6 -0
- data/lib/plurimath/math/function/table/multline.rb +6 -0
- data/lib/plurimath/math/function/table/pmatrix.rb +6 -0
- data/lib/plurimath/math/function/table/split.rb +6 -0
- data/lib/plurimath/math/function/table/vmatrix.rb +6 -0
- data/lib/plurimath/math/function/table.rb +10 -0
- data/lib/plurimath/math/function/tan.rb +6 -0
- data/lib/plurimath/math/function/tanh.rb +6 -0
- data/lib/plurimath/math/function/ternary_function.rb +4 -0
- data/lib/plurimath/math/function/text.rb +6 -0
- data/lib/plurimath/math/function/tilde.rb +6 -0
- data/lib/plurimath/math/function/ubrace.rb +6 -0
- data/lib/plurimath/math/function/ul.rb +6 -0
- data/lib/plurimath/math/function/unary_function.rb +4 -0
- data/lib/plurimath/math/function/uncertainty.rb +655 -0
- data/lib/plurimath/math/function/underover.rb +6 -0
- data/lib/plurimath/math/function/underset.rb +6 -0
- data/lib/plurimath/math/function/vec.rb +6 -0
- data/lib/plurimath/math/function.rb +1 -0
- data/lib/plurimath/math/renderer.rb +95 -0
- data/lib/plurimath/math/semantic_tree.rb +331 -0
- data/lib/plurimath/math/symbols/symbol.rb +12 -5
- data/lib/plurimath/math.rb +26 -0
- data/lib/plurimath/mathml/translator.rb +32 -0
- data/lib/plurimath/mathml/uncertainty_marker.rb +118 -0
- data/lib/plurimath/omml/translator.rb +68 -0
- data/lib/plurimath/omml/utility.rb +9 -1
- data/lib/plurimath/symbol_documentation.rb +57 -0
- data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +2 -1
- data/lib/plurimath/unicode_math/parsing_rules/masked.rb +3 -0
- data/lib/plurimath/unicode_math/transform.rb +64 -3
- data/lib/plurimath/unicode_math/utility.rb +38 -0
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +3 -0
- data/plurimath.gemspec +3 -1
- data/scripts/replay_asciimath_corpus.rb +611 -0
- metadata +30 -4
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
module Function
|
|
6
|
+
# A measured value with its uncertainty (plurimath#493).
|
|
7
|
+
#
|
|
8
|
+
# Three categories are supported: a standard uncertainty (JCGM 100
|
|
9
|
+
# 7.2.1, 7.2.2), a symmetric expanded uncertainty with its coverage
|
|
10
|
+
# factor (JCGM 100 6.2.1, 7.2.3) and a coverage interval given by its
|
|
11
|
+
# endpoints (JCGM 101 5.3, 5.5). The node records the reported numbers
|
|
12
|
+
# exactly as written; it never calculates, rounds or converts them.
|
|
13
|
+
# See docs/uncertainty.adoc.
|
|
14
|
+
class Uncertainty < Core
|
|
15
|
+
CATEGORIES = %i[standard expanded coverage_interval].freeze
|
|
16
|
+
NOTATIONS = %i[absolute concise].freeze
|
|
17
|
+
INTERVAL_KINDS = %i[shortest probabilistically_symmetric].freeze
|
|
18
|
+
|
|
19
|
+
SIGNED_DECIMAL = /\A-?[0-9]+(\.[0-9]+)?\z/
|
|
20
|
+
UNSIGNED_DECIMAL = /\A[0-9]+(\.[0-9]+)?\z/
|
|
21
|
+
|
|
22
|
+
MARKER_NAMES = {
|
|
23
|
+
standard: "standard-uncertainty",
|
|
24
|
+
expanded: "expanded-uncertainty",
|
|
25
|
+
coverage_interval: "coverage-interval",
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
ASCIIMATH_NAMES = {
|
|
29
|
+
standard: "ustd",
|
|
30
|
+
expanded: "uexp",
|
|
31
|
+
coverage_interval: "ucov",
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
INTERVAL_KIND_NAMES = {
|
|
35
|
+
shortest: "shortest",
|
|
36
|
+
probabilistically_symmetric: "probabilistically-symmetric",
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# AsciiMath reads "-" as minus, so the symmetric kind has its own spelling.
|
|
40
|
+
ASCIIMATH_INTERVAL_KIND_NAMES = {
|
|
41
|
+
shortest: "shortest",
|
|
42
|
+
probabilistically_symmetric: "symmetric",
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
PLUS_MINUS = "±"
|
|
46
|
+
|
|
47
|
+
# One unit symbol as UnitsML writes it, for example "kg".
|
|
48
|
+
UNIT_SYMBOL = /\A[A-Za-z]+\z/
|
|
49
|
+
|
|
50
|
+
attr_reader :value, :category, :magnitude, :coverage_factor,
|
|
51
|
+
:coverage_probability, :lower, :upper, :interval_kind,
|
|
52
|
+
:notation, :unit
|
|
53
|
+
|
|
54
|
+
# The keyword list is the documented model (docs/uncertainty.adoc).
|
|
55
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
56
|
+
value,
|
|
57
|
+
category:,
|
|
58
|
+
magnitude: nil,
|
|
59
|
+
coverage_factor: nil,
|
|
60
|
+
coverage_probability: nil,
|
|
61
|
+
lower: nil,
|
|
62
|
+
upper: nil,
|
|
63
|
+
interval_kind: nil,
|
|
64
|
+
notation: :absolute,
|
|
65
|
+
unit: nil
|
|
66
|
+
)
|
|
67
|
+
@value = value
|
|
68
|
+
@category = category
|
|
69
|
+
@magnitude = magnitude
|
|
70
|
+
@coverage_factor = coverage_factor
|
|
71
|
+
@coverage_probability = coverage_probability
|
|
72
|
+
@lower = lower
|
|
73
|
+
@upper = upper
|
|
74
|
+
@interval_kind = interval_kind
|
|
75
|
+
@notation = category == :standard ? notation : nil
|
|
76
|
+
@unit = unit
|
|
77
|
+
super()
|
|
78
|
+
validate!(notation)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ==(other)
|
|
82
|
+
other.class == self.class &&
|
|
83
|
+
other.category == category &&
|
|
84
|
+
other.value == value &&
|
|
85
|
+
other.magnitude == magnitude &&
|
|
86
|
+
other.coverage_factor == coverage_factor &&
|
|
87
|
+
other.coverage_probability == coverage_probability &&
|
|
88
|
+
other.lower == lower &&
|
|
89
|
+
other.upper == upper &&
|
|
90
|
+
other.interval_kind == interval_kind &&
|
|
91
|
+
other.notation == notation &&
|
|
92
|
+
other.unit == unit
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Core#cloned_objects builds `self.class.new(nil)`, which the
|
|
96
|
+
# validating constructor rejects, so rebuild through the constructor.
|
|
97
|
+
def cloned_objects
|
|
98
|
+
self.class.new(
|
|
99
|
+
value.cloned_objects,
|
|
100
|
+
category: category,
|
|
101
|
+
magnitude: magnitude&.cloned_objects,
|
|
102
|
+
coverage_factor: coverage_factor&.cloned_objects,
|
|
103
|
+
coverage_probability: coverage_probability&.cloned_objects,
|
|
104
|
+
lower: lower&.cloned_objects,
|
|
105
|
+
upper: upper&.cloned_objects,
|
|
106
|
+
interval_kind: interval_kind,
|
|
107
|
+
notation: notation || :absolute,
|
|
108
|
+
unit: unit&.cloned_objects,
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The node is one indivisible result; never split it across lines.
|
|
113
|
+
def line_breaking(_obj); end
|
|
114
|
+
|
|
115
|
+
# The marker string carried by MathML intent, the OMML phantom and
|
|
116
|
+
# the UnicodeMath intent form.
|
|
117
|
+
def marker
|
|
118
|
+
name = MARKER_NAMES[category]
|
|
119
|
+
name = "#{name}:concise" if notation == :concise
|
|
120
|
+
if interval_kind
|
|
121
|
+
name = "#{name}:#{INTERVAL_KIND_NAMES[interval_kind]}"
|
|
122
|
+
end
|
|
123
|
+
"#{name}(#{marker_arguments.join(',')})"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def concise_digits
|
|
127
|
+
digits = text(magnitude).delete(".").sub(/\A0+/, "")
|
|
128
|
+
digits.empty? ? "0" : digits
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Output
|
|
132
|
+
|
|
133
|
+
def to_asciimath(options:)
|
|
134
|
+
args = case category
|
|
135
|
+
when :standard
|
|
136
|
+
if notation == :concise
|
|
137
|
+
["#{text(value)}(#{concise_digits})"]
|
|
138
|
+
else
|
|
139
|
+
[text(value), text(magnitude)]
|
|
140
|
+
end
|
|
141
|
+
when :expanded
|
|
142
|
+
[text(value), text(magnitude), text(coverage_factor),
|
|
143
|
+
text(coverage_probability)]
|
|
144
|
+
else
|
|
145
|
+
[text(value), text(lower), text(upper),
|
|
146
|
+
text(coverage_probability),
|
|
147
|
+
ASCIIMATH_INTERVAL_KIND_NAMES[interval_kind]]
|
|
148
|
+
end
|
|
149
|
+
unitsml = asciimath_unitsml if unit
|
|
150
|
+
args << "\"unitsml(#{unitsml})\"" if unitsml
|
|
151
|
+
call = "#{ASCIIMATH_NAMES[category]}(#{args.compact.join(', ')})"
|
|
152
|
+
return call if unit.nil? || unitsml
|
|
153
|
+
|
|
154
|
+
# Not expressible in UnitsML: keep it visible after the call, the
|
|
155
|
+
# only place the AsciiMath reader accepts it (docs/uncertainty.adoc).
|
|
156
|
+
"#{call} #{unit.to_asciimath(options: options)}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_latex(options:)
|
|
160
|
+
unit_text = " #{unit.to_latex(options: options)}" if unit
|
|
161
|
+
case category
|
|
162
|
+
when :standard
|
|
163
|
+
"#{text(value)}(#{standard_parenthesised})#{unit_text}"
|
|
164
|
+
when :expanded
|
|
165
|
+
"(#{text(value)} \\pm #{text(magnitude)})#{unit_text}#{latex_statements}"
|
|
166
|
+
else
|
|
167
|
+
interval = "[#{text(lower)}, #{text(upper)}]"
|
|
168
|
+
"#{text(value)},\\ #{interval}#{unit_text}#{latex_statements}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def to_html(options:)
|
|
173
|
+
unit_text = " #{unit.to_html(options: options)}" if unit
|
|
174
|
+
case category
|
|
175
|
+
when :standard
|
|
176
|
+
"#{text(value)}(#{standard_parenthesised})#{unit_text}"
|
|
177
|
+
when :expanded
|
|
178
|
+
"(#{text(value)} #{PLUS_MINUS} #{text(magnitude)})#{unit_text}#{text_statements}"
|
|
179
|
+
else
|
|
180
|
+
interval = "[#{text(lower)}, #{text(upper)}]"
|
|
181
|
+
"#{text(value)}, #{interval}#{unit_text}#{text_statements}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def to_unicodemath(options:)
|
|
186
|
+
unit_text = "ⓐ(unit #{unit.to_unicodemath(options: options)})" if unit
|
|
187
|
+
visible = case category
|
|
188
|
+
when :standard
|
|
189
|
+
"#{unicodemath_value}(#{standard_parenthesised})#{unit_text}"
|
|
190
|
+
when :expanded
|
|
191
|
+
"(#{text(value)}±#{text(magnitude)})#{unit_text}#{unicodemath_statements}"
|
|
192
|
+
else
|
|
193
|
+
interval = "[#{text(lower)}, #{text(upper)}]"
|
|
194
|
+
"#{unicodemath_value}, #{interval}#{unit_text}#{unicodemath_statements}"
|
|
195
|
+
end
|
|
196
|
+
"ⓘ(\"#{marker}\"#{visible})"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def to_mathml_without_math_tag(intent, options:)
|
|
200
|
+
nodes = case category
|
|
201
|
+
when :standard
|
|
202
|
+
[
|
|
203
|
+
mn_tag(text(value)),
|
|
204
|
+
mathml_fence("(", ")", [mn_tag(standard_parenthesised)]),
|
|
205
|
+
]
|
|
206
|
+
when :expanded
|
|
207
|
+
[
|
|
208
|
+
mathml_fence(
|
|
209
|
+
"(", ")",
|
|
210
|
+
[mn_tag(text(value)), mo_tag(PLUS_MINUS),
|
|
211
|
+
mn_tag(text(magnitude))]
|
|
212
|
+
),
|
|
213
|
+
]
|
|
214
|
+
else
|
|
215
|
+
[
|
|
216
|
+
mn_tag(text(value)),
|
|
217
|
+
mo_tag(","),
|
|
218
|
+
mathml_fence(
|
|
219
|
+
"[", "]",
|
|
220
|
+
[mn_tag(text(lower)), mo_tag(","),
|
|
221
|
+
mn_tag(text(upper))]
|
|
222
|
+
),
|
|
223
|
+
]
|
|
224
|
+
end
|
|
225
|
+
nodes << mathml_unit(intent, options) if unit
|
|
226
|
+
nodes.concat(mathml_statements)
|
|
227
|
+
mrow = ox_element("mrow", attributes: { intent: marker })
|
|
228
|
+
XmlHelper.update_nodes(mrow, nodes)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def to_omml_without_math_tag(display_style, options:)
|
|
232
|
+
nodes = [omml_marker]
|
|
233
|
+
nodes.concat(
|
|
234
|
+
case category
|
|
235
|
+
when :standard
|
|
236
|
+
[
|
|
237
|
+
omml_run(text(value)),
|
|
238
|
+
omml_fence("(", ")", [omml_run(standard_parenthesised)]),
|
|
239
|
+
]
|
|
240
|
+
when :expanded
|
|
241
|
+
[
|
|
242
|
+
omml_fence(
|
|
243
|
+
"(", ")",
|
|
244
|
+
[omml_run(text(value)), omml_run(PLUS_MINUS),
|
|
245
|
+
omml_run(text(magnitude))]
|
|
246
|
+
),
|
|
247
|
+
]
|
|
248
|
+
else
|
|
249
|
+
[
|
|
250
|
+
omml_run(text(value)),
|
|
251
|
+
omml_run(","),
|
|
252
|
+
omml_fence(
|
|
253
|
+
"[", "]",
|
|
254
|
+
[omml_run(text(lower)), omml_run(","),
|
|
255
|
+
omml_run(text(upper))]
|
|
256
|
+
),
|
|
257
|
+
]
|
|
258
|
+
end,
|
|
259
|
+
)
|
|
260
|
+
nodes << omml_box(unit.insert_t_tag(display_style, options: options)) if unit
|
|
261
|
+
nodes.concat(statements.flat_map { |name, number| omml_statement(name, number) })
|
|
262
|
+
[omml_box(nodes)]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def to_asciimath_math_zone(spacing = "", _last = false, _indent = true,
|
|
266
|
+
options:)
|
|
267
|
+
"#{spacing}\"#{to_asciimath(options: options)}\" uncertainty\n"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def to_latex_math_zone(spacing = "", _last = false, _indent = true,
|
|
271
|
+
options:)
|
|
272
|
+
"#{spacing}\"#{to_latex(options: options)}\" uncertainty\n"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def to_mathml_math_zone(spacing = "", _last = false, _indent = true,
|
|
276
|
+
options:)
|
|
277
|
+
"#{spacing}\"#{dump_mathml(self, options: options)}\" uncertainty\n"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def to_omml_math_zone(spacing = "", _last = false, _indent = true,
|
|
281
|
+
display_style:, options:)
|
|
282
|
+
"#{spacing}\"#{dump_omml(self, display_style,
|
|
283
|
+
options: options)}\" uncertainty\n"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def to_unicodemath_math_zone(spacing = "", _last = false,
|
|
287
|
+
_indent = true, options:)
|
|
288
|
+
"#{spacing}\"#{to_unicodemath(options: options)}\" uncertainty\n"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def text(number)
|
|
294
|
+
number&.value
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def standard_parenthesised
|
|
298
|
+
notation == :concise ? concise_digits : text(magnitude)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def marker_arguments
|
|
302
|
+
args = case category
|
|
303
|
+
when :standard
|
|
304
|
+
[text(value), text(magnitude)]
|
|
305
|
+
when :expanded
|
|
306
|
+
[text(value), text(magnitude), text(coverage_factor),
|
|
307
|
+
text(coverage_probability)]
|
|
308
|
+
else
|
|
309
|
+
[text(value), text(lower), text(upper),
|
|
310
|
+
text(coverage_probability)]
|
|
311
|
+
end
|
|
312
|
+
args << "$unit" if unit
|
|
313
|
+
args.compact
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# The ", k = 2" and ", p = 0.95" statements, in order.
|
|
317
|
+
def statements
|
|
318
|
+
list = []
|
|
319
|
+
list << ["k", coverage_factor] if coverage_factor
|
|
320
|
+
list << ["p", coverage_probability] if coverage_probability
|
|
321
|
+
list
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def latex_statements
|
|
325
|
+
statements.map { |name, number| ",\\ #{name} = #{text(number)}" }.join
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def text_statements
|
|
329
|
+
statements.map { |name, number| ", #{name} = #{text(number)}" }.join
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def unicodemath_statements
|
|
333
|
+
statements.map { |name, number| ", #{name}=#{text(number)}" }.join
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The UnicodeMath reader cannot take `"marker"-...`, so a negative
|
|
337
|
+
# leading value goes in an invisible 〖〗 group; the marker is unchanged.
|
|
338
|
+
def unicodemath_value
|
|
339
|
+
text(value).start_with?("-") ? "〖#{text(value)}〗" : text(value)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The UnitsML text for the AsciiMath unit argument, or nil when the
|
|
343
|
+
# unit cannot be written as UnitsML. A unit read from markup is its
|
|
344
|
+
# rendered Formula; it is written back as UnitsML only when it is a
|
|
345
|
+
# `*`-separated product of upright unit symbols with optional integer
|
|
346
|
+
# powers, and UnitsML renders that text back to the same product.
|
|
347
|
+
def asciimath_unitsml
|
|
348
|
+
return unit.text if unit.is_a?(Unitsml)
|
|
349
|
+
|
|
350
|
+
candidate = unit_text(unit)
|
|
351
|
+
return unless candidate
|
|
352
|
+
|
|
353
|
+
candidate if unit_text(::Unitsml.parse(candidate).to_plurimath({})) == candidate
|
|
354
|
+
rescue *unitsml_parse_errors
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# What UnitsML raises for text it cannot read: Parslet::ParseFailed
|
|
359
|
+
# before 0.6.10, Parsanol::ParseFailed in 0.6.10 and 0.6.11, and its
|
|
360
|
+
# own Unitsml::Errors::ParseError after that.
|
|
361
|
+
def unitsml_parse_errors
|
|
362
|
+
[Parslet::ParseFailed,
|
|
363
|
+
(::Parsanol::ParseFailed if defined?(::Parsanol::ParseFailed)),
|
|
364
|
+
(::Unitsml::Errors::ParseError if defined?(::Unitsml::Errors::ParseError))]
|
|
365
|
+
.compact
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def unit_text(node)
|
|
369
|
+
case node
|
|
370
|
+
when Math::Formula then unit_product(node.value)
|
|
371
|
+
when Power then unit_power(node)
|
|
372
|
+
else unit_symbol(node)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def unit_product(parts)
|
|
377
|
+
return if parts.empty? || parts.length.even?
|
|
378
|
+
|
|
379
|
+
texts = parts.each_with_index.map do |part, index|
|
|
380
|
+
if index.odd?
|
|
381
|
+
part.is_a?(Math::Symbols::Cdot) ? "*" : nil
|
|
382
|
+
else
|
|
383
|
+
unit_text(part)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
texts.join unless texts.include?(nil)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def unit_power(node)
|
|
390
|
+
base = unit_symbol(node.parameter_one)
|
|
391
|
+
exponent = node.parameter_two
|
|
392
|
+
exponent = exponent.value if exponent.is_a?(Math::Formula)
|
|
393
|
+
exponent = Array(exponent)
|
|
394
|
+
sign = ""
|
|
395
|
+
if exponent.length == 2 && exponent.first.is_a?(Math::Symbols::Minus)
|
|
396
|
+
sign = "-"
|
|
397
|
+
exponent = exponent.drop(1)
|
|
398
|
+
end
|
|
399
|
+
number = exponent.first
|
|
400
|
+
return unless base && exponent.length == 1 &&
|
|
401
|
+
number.is_a?(Math::Number) && number.value.to_s.match?(/\A[0-9]+\z/)
|
|
402
|
+
|
|
403
|
+
"#{base}^#{sign}#{number.value}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def unit_symbol(node)
|
|
407
|
+
node = node.parameter_one if node.is_a?(FontStyle::Normal)
|
|
408
|
+
string = case node
|
|
409
|
+
when Text then node.parameter_one
|
|
410
|
+
when Math::Symbols::Symbol
|
|
411
|
+
node.value if node.instance_of?(Math::Symbols::Symbol)
|
|
412
|
+
end
|
|
413
|
+
string if string.is_a?(String) && string.match?(UNIT_SYMBOL)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# MathML helpers
|
|
417
|
+
|
|
418
|
+
def mn_tag(string)
|
|
419
|
+
ox_element("mn") << string
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def mi_tag(string)
|
|
423
|
+
ox_element("mi") << string
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def mathml_fence(open, close, nodes)
|
|
427
|
+
XmlHelper.update_nodes(
|
|
428
|
+
ox_element("mrow"),
|
|
429
|
+
[mo_tag(open), *nodes, mo_tag(close)],
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def mathml_statements
|
|
434
|
+
statements.flat_map do |name, number|
|
|
435
|
+
[mo_tag(","), mi_tag(name), mo_tag("="), mn_tag(text(number))]
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Unitsml marks its MathML with `unitsml`, so Formula#to_mathml puts
|
|
440
|
+
# the usual unit space in front of it; a unit read from markup is
|
|
441
|
+
# marked the same way.
|
|
442
|
+
def mathml_unit(intent, options)
|
|
443
|
+
nodes = unit.to_mathml_without_math_tag(intent, options: options)
|
|
444
|
+
mrow = if !nodes.is_a?(Array) && nodes.name == "mrow"
|
|
445
|
+
nodes
|
|
446
|
+
else
|
|
447
|
+
XmlHelper.update_nodes(ox_element("mrow"), Array(nodes))
|
|
448
|
+
end
|
|
449
|
+
mrow["unitsml"] = true unless mrow["unitsml"]
|
|
450
|
+
mrow["arg"] = "unit"
|
|
451
|
+
mrow
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# OMML helpers
|
|
455
|
+
|
|
456
|
+
def omml_run(string)
|
|
457
|
+
ox_element("r", namespace: "m") << (ox_element("t", namespace: "m") << string)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def omml_box(nodes)
|
|
461
|
+
box = ox_element("box", namespace: "m")
|
|
462
|
+
box << XmlHelper.update_nodes(ox_element("e", namespace: "m"), nodes)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def omml_fence(open, close, nodes)
|
|
466
|
+
dpr = ox_element("dPr", namespace: "m")
|
|
467
|
+
dpr << ox_element("begChr", namespace: "m", attributes: { "m:val": open })
|
|
468
|
+
dpr << ox_element("sepChr", namespace: "m", attributes: { "m:val": "" })
|
|
469
|
+
dpr << ox_element("endChr", namespace: "m", attributes: { "m:val": close })
|
|
470
|
+
d_tag = ox_element("d", namespace: "m") << dpr
|
|
471
|
+
d_tag << XmlHelper.update_nodes(ox_element("e", namespace: "m"), nodes)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def omml_statement(name, number)
|
|
475
|
+
[omml_run(","), omml_run(name), omml_run("="), omml_run(text(number))]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def omml_marker
|
|
479
|
+
phant_pr = ox_element("phantPr", namespace: "m")
|
|
480
|
+
phant_pr << ox_element("show", namespace: "m", attributes: { "m:val": "off" })
|
|
481
|
+
%w[zeroWid zeroAsc zeroDesc].each do |name|
|
|
482
|
+
phant_pr << ox_element(name, namespace: "m", attributes: { "m:val": "on" })
|
|
483
|
+
end
|
|
484
|
+
phant = ox_element("phant", namespace: "m") << phant_pr
|
|
485
|
+
phant << XmlHelper.update_nodes(
|
|
486
|
+
ox_element("e", namespace: "m"),
|
|
487
|
+
r_element(marker),
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Validation
|
|
492
|
+
|
|
493
|
+
def validate!(notation)
|
|
494
|
+
unless CATEGORIES.include?(category)
|
|
495
|
+
invalid!("unknown category #{category.inspect}")
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
validate_number(:value, value, signed: true)
|
|
499
|
+
%i[magnitude coverage_factor coverage_probability lower upper].each do |field|
|
|
500
|
+
number = public_send(field)
|
|
501
|
+
validate_number(field, number, signed: false) unless number.nil?
|
|
502
|
+
end
|
|
503
|
+
validate_unit
|
|
504
|
+
send(:"validate_#{category}", notation)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def validate_standard(notation)
|
|
508
|
+
forbid(:coverage_factor, :coverage_probability, :lower, :upper,
|
|
509
|
+
:interval_kind)
|
|
510
|
+
require_fields(:magnitude)
|
|
511
|
+
unless NOTATIONS.include?(notation)
|
|
512
|
+
invalid!("unknown notation #{notation.inspect}")
|
|
513
|
+
end
|
|
514
|
+
return unless notation == :concise
|
|
515
|
+
return if decimal_places(text(magnitude)) == decimal_places(text(value))
|
|
516
|
+
|
|
517
|
+
invalid!(
|
|
518
|
+
"magnitude #{text(magnitude)} cannot be written concisely after " \
|
|
519
|
+
"#{text(value)}: it needs as many decimal places as the value",
|
|
520
|
+
)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def validate_expanded(notation)
|
|
524
|
+
forbid(:lower, :upper, :interval_kind)
|
|
525
|
+
validate_unqualified_notation(notation)
|
|
526
|
+
require_fields(:magnitude, :coverage_factor)
|
|
527
|
+
validate_coverage_factor
|
|
528
|
+
validate_coverage_probability
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def validate_coverage_interval(notation)
|
|
532
|
+
forbid(:magnitude, :coverage_factor)
|
|
533
|
+
validate_unqualified_notation(notation)
|
|
534
|
+
require_fields(:lower, :upper, :coverage_probability)
|
|
535
|
+
unless interval_kind.nil? || INTERVAL_KINDS.include?(interval_kind)
|
|
536
|
+
invalid!("unknown interval kind #{interval_kind.inspect}")
|
|
537
|
+
end
|
|
538
|
+
validate_coverage_probability
|
|
539
|
+
return if compare_decimals(text(lower), text(upper)).negative?
|
|
540
|
+
|
|
541
|
+
invalid!("lower endpoint #{text(lower)} is not below upper endpoint #{text(upper)}")
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def validate_unqualified_notation(notation)
|
|
545
|
+
return if notation.nil? || notation == :absolute
|
|
546
|
+
|
|
547
|
+
invalid!("notation #{notation.inspect} applies only to a standard uncertainty")
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def validate_coverage_factor
|
|
551
|
+
return if positive?(text(coverage_factor))
|
|
552
|
+
|
|
553
|
+
invalid!("coverage factor #{text(coverage_factor)} must be greater than 0")
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def validate_coverage_probability
|
|
557
|
+
return if coverage_probability.nil?
|
|
558
|
+
|
|
559
|
+
probability = text(coverage_probability)
|
|
560
|
+
return if positive?(probability) && compare_decimals(probability, "1").negative?
|
|
561
|
+
|
|
562
|
+
invalid!("coverage probability #{probability} must lie strictly between 0 and 1")
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def validate_number(field, number, signed:)
|
|
566
|
+
pattern = signed ? SIGNED_DECIMAL : UNSIGNED_DECIMAL
|
|
567
|
+
kind = signed ? "decimal" : "unsigned decimal"
|
|
568
|
+
valid = number.is_a?(Math::Number) &&
|
|
569
|
+
number.value.is_a?(String) &&
|
|
570
|
+
number.value.match?(pattern) &&
|
|
571
|
+
!number.mini_sized? &&
|
|
572
|
+
number.base.nil?
|
|
573
|
+
return if valid
|
|
574
|
+
|
|
575
|
+
invalid!(
|
|
576
|
+
"#{field} must be a Math::Number holding a plain #{kind}, " \
|
|
577
|
+
"got #{number.inspect}",
|
|
578
|
+
)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def validate_unit
|
|
582
|
+
return if unit.nil? || unit.is_a?(Unitsml)
|
|
583
|
+
|
|
584
|
+
unless unit.is_a?(Math::Formula)
|
|
585
|
+
invalid!("unit must be a Math::Function::Unitsml, got #{unit.inspect}")
|
|
586
|
+
end
|
|
587
|
+
invalid!("unit must not be empty") if empty_formula?(unit)
|
|
588
|
+
return unless holds_uncertainty?(unit)
|
|
589
|
+
|
|
590
|
+
invalid!("unit must not contain an uncertainty")
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def empty_formula?(formula)
|
|
594
|
+
Array(formula.value).compact.all? do |part|
|
|
595
|
+
part.is_a?(Math::Formula) && empty_formula?(part)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def holds_uncertainty?(node)
|
|
600
|
+
case node
|
|
601
|
+
when Uncertainty then true
|
|
602
|
+
when Array then node.any? { |item| holds_uncertainty?(item) }
|
|
603
|
+
when Math::Core
|
|
604
|
+
node.instance_variables.any? do |name|
|
|
605
|
+
holds_uncertainty?(node.instance_variable_get(name))
|
|
606
|
+
end
|
|
607
|
+
else false
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def forbid(*fields)
|
|
612
|
+
fields.each do |field|
|
|
613
|
+
next if public_send(field).nil?
|
|
614
|
+
|
|
615
|
+
invalid!("#{field} does not apply to a #{category} uncertainty")
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def require_fields(*fields)
|
|
620
|
+
fields.each do |field|
|
|
621
|
+
next unless public_send(field).nil?
|
|
622
|
+
|
|
623
|
+
invalid!("#{field} is required for a #{category} uncertainty")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def invalid!(message)
|
|
628
|
+
raise Errors::InvalidUncertainty.new(message)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def decimal_places(string)
|
|
632
|
+
string.include?(".") ? string.split(".", 2).last.length : 0
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def positive?(string)
|
|
636
|
+
string.match?(/[1-9]/)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Exact comparison of two unsigned plain decimals, without Float.
|
|
640
|
+
def compare_decimals(first, second)
|
|
641
|
+
first_int, first_frac = first.split(".", 2)
|
|
642
|
+
second_int, second_frac = second.split(".", 2)
|
|
643
|
+
first_int = first_int.sub(/\A0+/, "")
|
|
644
|
+
second_int = second_int.sub(/\A0+/, "")
|
|
645
|
+
by_length = first_int.length <=> second_int.length
|
|
646
|
+
return by_length unless by_length.zero?
|
|
647
|
+
|
|
648
|
+
width = [first_frac.to_s.length, second_frac.to_s.length].max
|
|
649
|
+
"#{first_int}#{first_frac.to_s.ljust(width, '0')}" <=>
|
|
650
|
+
"#{second_int}#{second_frac.to_s.ljust(width, '0')}"
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
@@ -4,6 +4,12 @@ module Plurimath
|
|
|
4
4
|
module Math
|
|
5
5
|
module Function
|
|
6
6
|
class Underover < TernaryFunction
|
|
7
|
+
# --- Catalog documentation (see Plurimath::Documentation) ---
|
|
8
|
+
DESCRIPTION = "A base with expressions attached directly below and above it."
|
|
9
|
+
REFERENCE = "https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/munderover"
|
|
10
|
+
EXAMPLE = -> { new(sym("x"), sym("y"), sym("z")) }
|
|
11
|
+
# --- end catalog documentation ---
|
|
12
|
+
|
|
7
13
|
FUNCTION = {
|
|
8
14
|
name: "UnderOver",
|
|
9
15
|
first_value: "base",
|
|
@@ -4,6 +4,12 @@ module Plurimath
|
|
|
4
4
|
module Math
|
|
5
5
|
module Function
|
|
6
6
|
class Underset < BinaryFunction
|
|
7
|
+
# --- Catalog documentation (see Plurimath::Documentation) ---
|
|
8
|
+
DESCRIPTION = "An expression set as an underscript below a base."
|
|
9
|
+
REFERENCE = "https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/munder"
|
|
10
|
+
EXAMPLE = -> { new(sym("x"), sym("y")) }
|
|
11
|
+
# --- end catalog documentation ---
|
|
12
|
+
|
|
7
13
|
attr_accessor :options
|
|
8
14
|
|
|
9
15
|
FUNCTION = {
|
|
@@ -4,6 +4,12 @@ module Plurimath
|
|
|
4
4
|
module Math
|
|
5
5
|
module Function
|
|
6
6
|
class Vec < UnaryFunction
|
|
7
|
+
# --- Catalog documentation (see Plurimath::Documentation) ---
|
|
8
|
+
DESCRIPTION = "A right-arrow (vector) accent over an expression."
|
|
9
|
+
REFERENCE = "https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mover"
|
|
10
|
+
EXAMPLE = -> { new(sym("x")) }
|
|
11
|
+
# --- end catalog documentation ---
|
|
12
|
+
|
|
7
13
|
attr_accessor :attributes
|
|
8
14
|
|
|
9
15
|
def initialize(parameter_one = nil, attributes = {})
|