dcc 0.2.0 → 0.2.2

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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +19 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop_todo.yml +560 -10
  6. data/CHANGELOG.adoc +34 -1
  7. data/CONTRIBUTING.adoc +8 -1
  8. data/Gemfile +5 -7
  9. data/README.adoc +162 -7
  10. data/dcc.gemspec +9 -2
  11. data/docs/schema-versions.adoc +20 -7
  12. data/examples/build_minimal_dcc.rb +0 -0
  13. data/examples/convert_all_formats.rb +2 -1
  14. data/examples/parse_and_inspect.rb +0 -0
  15. data/examples/quantity_math_demo.rb +7 -7
  16. data/examples/validate_full.rb +1 -1
  17. data/lib/dcc/base/calibration_laboratory.rb +2 -1
  18. data/lib/dcc/base/condition.rb +5 -2
  19. data/lib/dcc/base/core_data.rb +8 -3
  20. data/lib/dcc/base/formula.rb +3 -3
  21. data/lib/dcc/base/identification.rb +6 -3
  22. data/lib/dcc/base/mathml.rb +33 -0
  23. data/lib/dcc/base/quantity.rb +2 -1
  24. data/lib/dcc/base/resp_person.rb +4 -2
  25. data/lib/dcc/base/text.rb +1 -1
  26. data/lib/dcc/base.rb +2 -1
  27. data/lib/dcc/builder/dsl.rb +1 -1
  28. data/lib/dcc/builder/session.rb +16 -14
  29. data/lib/dcc/builder.rb +3 -3
  30. data/lib/dcc/cli/cli.rb +152 -26
  31. data/lib/dcc/cli/formatters.rb +35 -7
  32. data/lib/dcc/cli.rb +1 -1
  33. data/lib/dcc/context_configuration.rb +2 -1
  34. data/lib/dcc/context_options.rb +4 -2
  35. data/lib/dcc/convert/csv.rb +27 -9
  36. data/lib/dcc/convert/html.rb +26 -110
  37. data/lib/dcc/convert/json.rb +10 -7
  38. data/lib/dcc/convert/liquid_filters.rb +58 -0
  39. data/lib/dcc/convert/result.rb +1 -1
  40. data/lib/dcc/convert/templates/report.liquid +250 -0
  41. data/lib/dcc/convert/yaml.rb +1 -1
  42. data/lib/dcc/convert.rb +1 -0
  43. data/lib/dcc/diff/change.rb +1 -1
  44. data/lib/dcc/diff/result.rb +8 -4
  45. data/lib/dcc/diff.rb +43 -11
  46. data/lib/dcc/error.rb +7 -0
  47. data/lib/dcc/extract/file.rb +3 -4
  48. data/lib/dcc/extract/formula/ast.rb +137 -0
  49. data/lib/dcc/extract/formula/evaluator.rb +357 -0
  50. data/lib/dcc/extract/formula/parser.rb +305 -0
  51. data/lib/dcc/extract/formula/quantity.rb +89 -0
  52. data/lib/dcc/extract/formula.rb +110 -0
  53. data/lib/dcc/extract/ring.rb +1 -1
  54. data/lib/dcc/extract.rb +1 -1
  55. data/lib/dcc/i18n/text_lookup.rb +8 -4
  56. data/lib/dcc/i18n.rb +1 -1
  57. data/lib/dcc/inspect/report.rb +51 -14
  58. data/lib/dcc/inspect.rb +1 -1
  59. data/lib/dcc/migrate/route.rb +44 -0
  60. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  61. data/lib/dcc/migrate.rb +130 -26
  62. data/lib/dcc/plugin/base.rb +64 -0
  63. data/lib/dcc/plugin/registry.rb +7 -2
  64. data/lib/dcc/plugin.rb +6 -2
  65. data/lib/dcc/quantity_format/formatter.rb +16 -10
  66. data/lib/dcc/quantity_format.rb +1 -1
  67. data/lib/dcc/quantity_math/complex.rb +252 -0
  68. data/lib/dcc/quantity_math/matrix.rb +217 -0
  69. data/lib/dcc/quantity_math/quantity.rb +1 -1
  70. data/lib/dcc/quantity_math/real.rb +6 -6
  71. data/lib/dcc/quantity_math.rb +31 -3
  72. data/lib/dcc/schema/version.rb +2 -2
  73. data/lib/dcc/server/app.html +282 -0
  74. data/lib/dcc/server/app.rb +171 -87
  75. data/lib/dcc/server/demo_cert.xml +244 -0
  76. data/lib/dcc/server/storage/entry.rb +1 -1
  77. data/lib/dcc/server/storage/memory.rb +3 -2
  78. data/lib/dcc/server/storage.rb +1 -1
  79. data/lib/dcc/server.rb +1 -1
  80. data/lib/dcc/si/base/complex.rb +1 -1
  81. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  82. data/lib/dcc/si/base/constant.rb +1 -1
  83. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  84. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  85. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  86. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  87. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  88. data/lib/dcc/si/base/hybrid.rb +3 -2
  89. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  90. data/lib/dcc/si/base/real.rb +13 -3
  91. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  92. data/lib/dcc/si/base/si_list.rb +3 -2
  93. data/lib/dcc/si/base/standard_mu.rb +1 -1
  94. data/lib/dcc/si/base.rb +5 -2
  95. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  96. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  97. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  98. data/lib/dcc/si/v1.rb +5 -2
  99. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  100. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  101. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  102. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  103. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  104. data/lib/dcc/si/v2.rb +8 -3
  105. data/lib/dcc/signature/result.rb +2 -2
  106. data/lib/dcc/signature/signer.rb +31 -18
  107. data/lib/dcc/signature/verifier.rb +23 -21
  108. data/lib/dcc/signature.rb +1 -1
  109. data/lib/dcc/streaming/reader.rb +246 -0
  110. data/lib/dcc/streaming.rb +79 -0
  111. data/lib/dcc/transform/result.rb +1 -1
  112. data/lib/dcc/transform/xslt.rb +22 -12
  113. data/lib/dcc/transform.rb +5 -4
  114. data/lib/dcc/type/base64_binary.rb +5 -3
  115. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  116. data/lib/dcc/type/iso_country_code.rb +3 -2
  117. data/lib/dcc/type/iso_language_code.rb +3 -2
  118. data/lib/dcc/type/schema_version.rb +3 -2
  119. data/lib/dcc/type/si_unit.rb +3 -2
  120. data/lib/dcc/type_guards.rb +3 -3
  121. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v2/condition.rb +22 -1
  123. data/lib/dcc/v2/configuration.rb +1 -1
  124. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  125. data/lib/dcc/v2/identification.rb +14 -1
  126. data/lib/dcc/v2/mathml.rb +8 -0
  127. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  128. data/lib/dcc/v2/quantity.rb +4 -2
  129. data/lib/dcc/v2.rb +4 -2
  130. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  131. data/lib/dcc/v3/configuration.rb +1 -1
  132. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  133. data/lib/dcc/v3/mathml.rb +8 -0
  134. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  135. data/lib/dcc/v3/quantity.rb +4 -2
  136. data/lib/dcc/v3.rb +12 -2
  137. data/lib/dcc/validate/business_rules.rb +23 -9
  138. data/lib/dcc/validate/issue.rb +2 -1
  139. data/lib/dcc/validate/result.rb +6 -4
  140. data/lib/dcc/validate/schematron/profile.rb +73 -18
  141. data/lib/dcc/validate/schematron/rule.rb +13 -3
  142. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  143. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  144. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  145. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  146. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  147. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  148. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  149. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  150. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  151. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  152. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  153. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  154. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  155. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  156. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  157. data/lib/dcc/validate/schematron/rules.rb +28 -14
  158. data/lib/dcc/validate/schematron.rb +1 -1
  159. data/lib/dcc/validate/xsd.rb +9 -3
  160. data/lib/dcc/validate.rb +1 -1
  161. data/lib/dcc/version.rb +1 -1
  162. data/lib/dcc/versioned_parser.rb +1 -1
  163. data/lib/dcc.rb +68 -10
  164. data/sample_dcc.xml +140 -0
  165. data/web/index.html +12 -0
  166. data/web/package-lock.json +1599 -0
  167. data/web/package.json +22 -0
  168. data/web/src/App.vue +349 -0
  169. data/web/src/lib/builder.ts +171 -0
  170. data/web/src/lib/dcc.ts +338 -0
  171. data/web/src/lib/units.json +5581 -0
  172. data/web/src/main.ts +5 -0
  173. data/web/src/style.css +27 -0
  174. data/web/tsconfig.json +15 -0
  175. data/web/vite.config.ts +14 -0
  176. metadata +148 -9
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Dcc
6
+ module Extract
7
+ module Formula
8
+ # Turns an `Mml::V3::Math` model into a `Formula::Ast`.
9
+ #
10
+ # mml splits an element's children into one collection per child type
11
+ # (`apply_value`, `ci_value`, …), which loses order across types.
12
+ # `element_order` records the original child sequence, so we walk that
13
+ # and pop from the matching collection with a per-name cursor.
14
+ module Parser
15
+ OPERATORS = {
16
+ "plus" => :+,
17
+ "minus" => :-,
18
+ "times" => :*,
19
+ "divide" => :/,
20
+ "power" => :**,
21
+ "root" => :root,
22
+ "exp" => :exp,
23
+ "ln" => :ln,
24
+ "log" => :log,
25
+ "sin" => :sin,
26
+ "cos" => :cos,
27
+ "tan" => :tan,
28
+ "abs" => :abs,
29
+ }.freeze
30
+
31
+ CONSTANTS = { "pi" => :pi, "exponentiale" => :e }.freeze
32
+
33
+ # `<cn>` encodings whose value is a single decimal literal. Every
34
+ # other type spreads the number across `<sep/>`-separated parts.
35
+ PLAIN_CN_TYPES = %w[real integer double].freeze
36
+
37
+ class << self
38
+ # @param math [Mml::V3::Math]
39
+ # @param bindings [Hash{String => Quantity}]
40
+ # @return [Dcc::Extract::Formula::Ast]
41
+ def call(math, bindings: {})
42
+ name, model = only(children(math))
43
+ return parse_declaration(model, bindings) if name == "declare"
44
+
45
+ Ast.new(name: nil, bound_variables: [],
46
+ body: node(name, model, []), bindings: bindings)
47
+ end
48
+
49
+ private
50
+
51
+ def parse_declaration(declaration, bindings)
52
+ nodes = children(declaration)
53
+ check_declaration(nodes)
54
+ lambda_children = children(fetch(nodes, "lambda"))
55
+ bound = bound_variable_names(lambda_children)
56
+ body = expression(without_bvars(lambda_children), bound)
57
+ Ast.new(name: text_of(nodes, "ci"), bound_variables: bound,
58
+ body: body, bindings: bindings)
59
+ end
60
+
61
+ # `fetch` and `text_of` take the first match, so a second
62
+ # <lambda> or a second name <ci> would vanish the same way a
63
+ # second top-level expression used to.
64
+ def check_declaration(nodes)
65
+ names = nodes.map(&:first)
66
+ return if names == %w[ci lambda]
67
+
68
+ raise ::Dcc::ExtractionError,
69
+ "<declare> must hold one <ci> then one <lambda>, " \
70
+ "found #{names.empty? ? 'nothing' : names.join(', ')}"
71
+ end
72
+
73
+ def without_bvars(nodes)
74
+ nodes.reject { |pair| pair.first == "bvar" }
75
+ end
76
+
77
+ def bound_variable_names(nodes)
78
+ nodes.select { |pair| pair.first == "bvar" }
79
+ .map { |pair| bound_name(children(pair.last)) }
80
+ end
81
+
82
+ # A <bvar> names one variable. MathML also allows a <degree>
83
+ # qualifier there, but that spells a derivative and this
84
+ # evaluator has none to offer, so it is refused rather than
85
+ # ignored — and the message names what was actually there.
86
+ def bound_name(contents)
87
+ names = contents.map(&:first)
88
+ return text(contents.first.last) if names == %w[ci]
89
+
90
+ raise ::Dcc::ExtractionError,
91
+ "<bvar> must contain exactly one <ci>, " \
92
+ "found #{names.empty? ? 'nothing' : names.join(', ')}"
93
+ end
94
+
95
+ # @param nodes [Array<(String, Object)>] ordered
96
+ # [element name, model] pairs.
97
+ # @return [Object] the single expression node.
98
+ def expression(nodes, bound = [])
99
+ name, model = only(nodes)
100
+ node(name, model, bound)
101
+ end
102
+
103
+ # `<math>` and a `<lambda>` body each hold exactly one
104
+ # expression. Taking `.first` of several dropped the rest
105
+ # without a word — the same silent wrong answer this file
106
+ # refuses everywhere else. (`<bvar>` has its own check, in
107
+ # `bound_name`, because a qualifier there is not an
108
+ # expression.)
109
+ def only(nodes)
110
+ case nodes.size
111
+ when 1 then nodes.first
112
+ when 0
113
+ raise ::Dcc::ExtractionError,
114
+ "dcc:formula contains no formula expression"
115
+ else
116
+ raise ::Dcc::ExtractionError,
117
+ "dcc:formula contains #{nodes.size} expressions " \
118
+ "where exactly one is expected"
119
+ end
120
+ end
121
+
122
+ def node(name, model, bound)
123
+ case name
124
+ when "apply" then apply(model, bound)
125
+ when "cn" then number_node(model)
126
+ when "ci" then variable(text(model), bound)
127
+ else constant_node(name)
128
+ end
129
+ end
130
+
131
+ # A structured `<cn>` splits its number across text segments
132
+ # separated by `<sep/>`, and `text` concatenates them, so a
133
+ # rational `1<sep/>2` would silently read as 12 rather than one
134
+ # half. A wrong answer is worse than a refusal, so encodings we
135
+ # do not implement are rejected outright.
136
+ #
137
+ # No explicit coercion here — Ast::Number's constructor runs the
138
+ # text through Quantity.decimal.
139
+ def number_node(model)
140
+ check_plain_number(model)
141
+ literal = text(model)
142
+ number = Ast::Number.new(value: literal)
143
+ check_integral(model.type, number.value, literal)
144
+ number
145
+ end
146
+
147
+ # `<cn type="integer">1.5</cn>` declares one thing and writes
148
+ # another. Reading it as 1.5 is the same contradiction
149
+ # `base="8"` is refused for.
150
+ #
151
+ # Returns the node explicitly rather than ending on the check:
152
+ # a check's return value in an operand slot would put nil into
153
+ # the tree and leak a raw NoMethodError out of `abs`.
154
+ # Quotes the source lexeme, not `value.to_s("F")`: expanding
155
+ # the decimal turns `1e-100000` — nine bytes of document —
156
+ # into a hundred-kilobyte exception message.
157
+ def check_integral(type, value, literal)
158
+ return unless type == "integer"
159
+ return if value.frac.zero?
160
+
161
+ raise ::Dcc::ExtractionError,
162
+ %(<cn type="integer"> holds a non-integer value: #{literal})
163
+ end
164
+
165
+ def check_plain_number(model)
166
+ check_base(model)
167
+ unless model.sep_value.empty?
168
+ raise ::Dcc::ExtractionError,
169
+ "Structured <cn> with <sep/> is not supported"
170
+ end
171
+ return if model.type.nil? || PLAIN_CN_TYPES.include?(model.type)
172
+
173
+ raise ::Dcc::ExtractionError,
174
+ "Unsupported <cn> type: #{model.type}"
175
+ end
176
+
177
+ # `<cn base="8">17</cn>` is octal — fifteen, not seventeen.
178
+ # Reading any other base as decimal is the same silent wrong
179
+ # number the `<sep/>` check refuses, so only base 10 is accepted.
180
+ def check_base(model)
181
+ return if model.base.nil? || model.base == "10"
182
+
183
+ raise ::Dcc::ExtractionError,
184
+ "Unsupported <cn> base: #{model.base}"
185
+ end
186
+
187
+ def constant_node(name)
188
+ constant = CONSTANTS.fetch(name) do
189
+ raise ::Dcc::ExtractionError,
190
+ "Unsupported MathML element: #{name}"
191
+ end
192
+ Ast::Constant.new(name: constant)
193
+ end
194
+
195
+ def apply(model, bound)
196
+ (operator_name,), *operands = children(model)
197
+ operator = OPERATORS.fetch(operator_name) do
198
+ raise ::Dcc::ExtractionError,
199
+ "Unsupported MathML operator: #{operator_name}"
200
+ end
201
+ Ast::Apply.new(
202
+ operator: operator,
203
+ operands: operand_nodes(operator, operands, bound),
204
+ )
205
+ end
206
+
207
+ def operand_nodes(operator, operands, bound)
208
+ return root_operands(operands, bound) if operator == :root
209
+
210
+ operands.map { |name, child| node(name, child, bound) }
211
+ end
212
+
213
+ # MathML puts an optional `<degree>` wrapper before the radicand.
214
+ # Unwrap it and store operands as [value, degree]. Cardinality is
215
+ # enforced here: silently taking `.first` of several radicands or
216
+ # several degrees would corrupt the evaluator's operand ordering.
217
+ def root_operands(operands, bound)
218
+ degrees, values =
219
+ operands.partition { |pair| pair.first == "degree" }
220
+ check_root_arity(values, degrees)
221
+
222
+ name, child = values.first
223
+ nodes = [node(name, child, bound)]
224
+ return nodes if degrees.empty?
225
+
226
+ nodes + [degree_node(degrees.first.last, bound)]
227
+ end
228
+
229
+ def check_root_arity(values, degrees)
230
+ unless values.size == 1
231
+ raise ::Dcc::ExtractionError,
232
+ "<root> must have exactly one radicand, " \
233
+ "found #{values.size}"
234
+ end
235
+ return unless degrees.size > 1
236
+
237
+ raise ::Dcc::ExtractionError,
238
+ "<root> must have at most one <degree>, " \
239
+ "found #{degrees.size}"
240
+ end
241
+
242
+ def degree_node(model, bound)
243
+ contents = children(model)
244
+ unless contents.size == 1
245
+ raise ::Dcc::ExtractionError,
246
+ "<degree> must contain exactly one expression, " \
247
+ "found #{contents.size}"
248
+ end
249
+
250
+ name, child = contents.first
251
+ node(name, child, bound)
252
+ end
253
+
254
+ def variable(name, bound)
255
+ return Ast::BoundVariable.new(name: name) if bound.include?(name)
256
+
257
+ Ast::Variable.new(name: name)
258
+ end
259
+
260
+ # Rebuild the original child sequence from `element_order`.
261
+ # @return [Array<(String, Object)>]
262
+ def children(model)
263
+ cursors = ::Hash.new(0)
264
+ elements = model.element_order.select do |e|
265
+ e.node_type == :element
266
+ end
267
+ elements.map do |element|
268
+ name = element.name
269
+ child = collection(model, name)[cursors[name]]
270
+ cursors[name] += 1
271
+ [name, child]
272
+ end
273
+ end
274
+
275
+ # `element_order` can name any MathML element; only the ones mml
276
+ # models have a `*_value` reader. This is a boundary check against
277
+ # the gem, not defensive padding.
278
+ def collection(model, name)
279
+ reader = :"#{name.tr('-', '_')}_value"
280
+ unless model.respond_to?(reader)
281
+ raise ::Dcc::ExtractionError,
282
+ "Unsupported MathML element: #{name}"
283
+ end
284
+
285
+ model.public_send(reader)
286
+ end
287
+
288
+ def fetch(nodes, name)
289
+ nodes.assoc(name)&.last ||
290
+ raise(::Dcc::ExtractionError,
291
+ "Expected a <#{name}> element in dcc:formula")
292
+ end
293
+
294
+ def text_of(nodes, name)
295
+ text(fetch(nodes, name))
296
+ end
297
+
298
+ def text(model)
299
+ Array(model.value).join.strip
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Dcc
6
+ module Extract
7
+ module Formula
8
+ # A named numeric binding taken from a `dcc:quantity`. `value` is a
9
+ # single `BigDecimal` for `si:real` and an array for the `si:*XMLList`
10
+ # forms. `uncertainty` is part of the shape callers see, but nothing
11
+ # populates it yet — extraction always passes nil.
12
+ #
13
+ # `Quantity.decimal` is the one place untrusted input becomes a
14
+ # BigDecimal. The constructor runs every value through it, so a
15
+ # Quantity cannot exist holding a non-finite or non-numeric value.
16
+ # That invariant is what lets `Evaluator#quantify` trust its contents.
17
+ Quantity = ::Data.define(:value, :unit, :uncertainty) do
18
+ # The single coercion boundary for the whole formula pipeline: the
19
+ # parser's `<cn>` text, the extractor's `si:value` (declared merely
20
+ # `:string`, so it can hold anything), and the evaluator's overrides
21
+ # all land here.
22
+ #
23
+ # @param input [Object] anything that might be a number.
24
+ # @return [BigDecimal] finite, always.
25
+ # @raise [Dcc::ExtractionError] if it is not a finite number.
26
+ def self.decimal(input)
27
+ number = input.is_a?(::BigDecimal) ? input : from_text(input)
28
+ unless number.finite?
29
+ raise ::Dcc::ExtractionError,
30
+ "Formula value is not a finite number: #{input.inspect}"
31
+ end
32
+
33
+ number
34
+ rescue ArgumentError
35
+ raise ::Dcc::ExtractionError,
36
+ "Formula value is not a number: #{input.inspect}"
37
+ end
38
+
39
+ # `BigDecimal("1_000")` quietly reads 1000, but no document writes a
40
+ # number that way. Malformed text, not a value ten times its face.
41
+ def self.from_text(input)
42
+ text = input.to_s
43
+ raise ::ArgumentError if text.include?("_")
44
+
45
+ BigDecimal(text)
46
+ end
47
+ private_class_method :from_text
48
+
49
+ # Lists are narrowed with `Array.try_convert`, not `is_a?(Array)`:
50
+ # the D-SI branch hands back a `Dcc::Type::DecimalXmlList::Values`,
51
+ # which is array-like but is not an Array. `try_convert` also
52
+ # rejects an object whose `to_ary` breaks the protocol, which a
53
+ # bare `respond_to?(:to_ary)` would pass through to a NoMethodError.
54
+ #
55
+ # `Data` freezes the struct but not what it holds, so the coerced
56
+ # list is frozen too.
57
+ def initialize(value:, unit: nil, uncertainty: nil)
58
+ super(value: Quantity.coerce(value), unit: unit,
59
+ uncertainty: uncertainty)
60
+ end
61
+
62
+ def self.coerce(value)
63
+ return decimal(value) unless value.respond_to?(:to_ary)
64
+
65
+ list = ::Array.try_convert(value)
66
+ raise ::Dcc::ExtractionError, broken_list(value) if list.nil?
67
+
68
+ list.map { |item| decimal(item) }.freeze
69
+ rescue ::TypeError
70
+ raise ::Dcc::ExtractionError, broken_list(value)
71
+ end
72
+
73
+ def self.broken_list(value)
74
+ "Formula value is not a usable list: #{value.class}"
75
+ end
76
+
77
+ # @return [Array<BigDecimal>]
78
+ def values
79
+ list? ? value : [value]
80
+ end
81
+
82
+ # @return [Boolean]
83
+ def list?
84
+ value.is_a?(::Array)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Dcc
6
+ module Extract
7
+ # `Dcc::Extract::Formula` pulls content-MathML formulae out of a parsed
8
+ # DCC. Each `dcc:formula` that carries a `dcc:mathml` becomes one `Ast`,
9
+ # bound to the `dcc:quantity` values sitting in the same `dcc:data`
10
+ # block — that is where PTB puts a formula's parameters and inputs.
11
+ module Formula
12
+ autoload :Ast, "dcc/extract/formula/ast"
13
+ autoload :Evaluator, "dcc/extract/formula/evaluator"
14
+ autoload :Parser, "dcc/extract/formula/parser"
15
+ autoload :Quantity, "dcc/extract/formula/quantity"
16
+
17
+ class << self
18
+ # @param dcc [Lutaml::Model::Serializable] a parsed DCC.
19
+ # @return [Array<Dcc::Extract::Formula::Ast>] in document order.
20
+ def call(dcc)
21
+ data_blocks(dcc).flat_map { |data| formulae_in(data) }
22
+ end
23
+
24
+ private
25
+
26
+ def formulae_in(data)
27
+ bindings = bindings_in(data)
28
+ data.formula.filter_map do |formula|
29
+ math = formula.mathml&.math
30
+ Parser.call(math, bindings: bindings) if math
31
+ end
32
+ end
33
+
34
+ # Parameters live in `dcc:data/dcc:list/dcc:quantity`, but the XSD
35
+ # also allows a bare `dcc:quantity` directly under `dcc:data`.
36
+ def bindings_in(data)
37
+ quantities_in(data).each_with_object({}) do |quantity, bindings|
38
+ next unless quantity.id
39
+
40
+ value = quantity_value(quantity)
41
+ bindings[quantity.id] = value if value
42
+ end
43
+ end
44
+
45
+ # `dcc:listType` is recursive, and both it and `dcc:dataType`
46
+ # expose `quantity` and `list`. Walking only the first level of
47
+ # lists loses every binding nested below it.
48
+ def quantities_in(container)
49
+ container.quantity +
50
+ container.list.flat_map { |list| quantities_in(list) }
51
+ end
52
+
53
+ # `si:value` is declared `attribute :value, :string` in
54
+ # `lib/dcc/si/base/real.rb` — completely unvalidated, so a real
55
+ # document can carry "banana", "Infinity", or nothing at all. Hand
56
+ # it to Quantity untouched and let the one coercion boundary decide;
57
+ # calling `BigDecimal` here would leak a raw ArgumentError out of a
58
+ # public extraction call.
59
+ #
60
+ # Both `real` and `real_list_xml_list` are collection attributes on
61
+ # `Dcc::V3::Quantity`, so both need `.first`.
62
+ def quantity_value(quantity)
63
+ real = quantity.real.first
64
+ return scalar(real) if real
65
+
66
+ list = quantity.real_list_xml_list.first
67
+ list_values(list) if list
68
+ end
69
+
70
+ def scalar(real)
71
+ Quantity.new(value: real.value, unit: real.unit, uncertainty: nil)
72
+ end
73
+
74
+ def list_values(list)
75
+ Quantity.new(
76
+ value: list.value_xml_list,
77
+ unit: list.unit_xml_list,
78
+ uncertainty: nil,
79
+ )
80
+ end
81
+
82
+ # Same shape as `Dcc::Extract::File.each`: walk the model graph and
83
+ # collect every `dcc:data` node in document order.
84
+ def data_blocks(dcc)
85
+ blocks = []
86
+ walk(dcc, ::Set.new, blocks)
87
+ blocks
88
+ end
89
+
90
+ # `Set#add?` returns nil when already present, so it doubles as the
91
+ # cycle guard for the self-referential list types.
92
+ def walk(node, visited, blocks)
93
+ return unless node.is_a?(::Lutaml::Model::Serializable)
94
+ return unless visited.add?(node.object_id)
95
+
96
+ blocks << node if data?(node)
97
+ node.class.attributes.each_key do |attr_name|
98
+ Array(node.public_send(attr_name)).each do |child|
99
+ walk(child, visited, blocks)
100
+ end
101
+ end
102
+ end
103
+
104
+ def data?(node)
105
+ node.class.name.end_with?("::Data")
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -25,4 +25,4 @@ module Dcc
25
25
  end
26
26
  end
27
27
  end
28
- end
28
+ end
data/lib/dcc/extract.rb CHANGED
@@ -6,4 +6,4 @@ module Dcc
6
6
  autoload :Ring, "dcc/extract/ring"
7
7
  autoload :Formula, "dcc/extract/formula"
8
8
  end
9
- end
9
+ end
@@ -18,7 +18,9 @@ module Dcc
18
18
  # @param lang [String, nil] requested ISO 639-1 code.
19
19
  # @return [String, nil]
20
20
  def call(text_obj, dcc: nil, lang: nil)
21
- return nil unless text_obj && Dcc::TypeGuards.has_attribute?(text_obj, :content)
21
+ return nil unless text_obj && Dcc::TypeGuards.has_attribute?(
22
+ text_obj, :content
23
+ )
22
24
 
23
25
  contents = Array(text_obj.content)
24
26
  return nil if contents.empty?
@@ -49,10 +51,12 @@ module Dcc
49
51
  end
50
52
 
51
53
  def declared_languages(dcc)
52
- return [] unless dcc && Dcc::TypeGuards.has_attribute?(dcc, :administrative_data)
54
+ return [] unless dcc && Dcc::TypeGuards.has_attribute?(dcc,
55
+ :administrative_data)
53
56
 
54
57
  admin = dcc.administrative_data
55
- return [] unless admin && Dcc::TypeGuards.has_attribute?(admin, :core_data)
58
+ return [] unless admin && Dcc::TypeGuards.has_attribute?(admin,
59
+ :core_data)
56
60
 
57
61
  core = admin.core_data
58
62
  return [] unless core
@@ -64,4 +68,4 @@ module Dcc
64
68
  end
65
69
  end
66
70
  end
67
- end
71
+ end
data/lib/dcc/i18n.rb CHANGED
@@ -9,4 +9,4 @@ module Dcc
9
9
  module I18n
10
10
  autoload :TextLookup, "dcc/i18n/text_lookup"
11
11
  end
12
- end
12
+ end
@@ -23,7 +23,7 @@ module Dcc
23
23
  def to_s
24
24
  lines = []
25
25
  lines << "DCC Inspection Report"
26
- lines << "=" * 50
26
+ lines << ("=" * 50)
27
27
  lines << "Schema version: #{schema_version}"
28
28
  lines << "Unique identifier: #{unique_identifier}"
29
29
  lines << "Country: #{country_code}"
@@ -56,7 +56,7 @@ module Dcc
56
56
  # @param dcc [Lutaml::Model::Serializable]
57
57
  # @return [Dcc::Inspect::Report]
58
58
  def call(dcc)
59
- admin = admin_data(dcc)
59
+ admin_data(dcc)
60
60
  mr_list = measurement_results(dcc)
61
61
  new(
62
62
  schema_version: dcc.schema_version.to_s,
@@ -76,42 +76,75 @@ module Dcc
76
76
  private
77
77
 
78
78
  def admin_data(dcc)
79
- Dcc::TypeGuards.has_attribute?(dcc, :administrative_data) ? dcc.administrative_data : nil
79
+ if Dcc::TypeGuards.has_attribute?(dcc,
80
+ :administrative_data)
81
+ dcc.administrative_data
82
+ end
80
83
  end
81
84
 
82
85
  def core_data(dcc)
83
86
  ad = admin_data(dcc)
84
- ad && Dcc::TypeGuards.has_attribute?(ad, :core_data) ? ad.core_data : nil
87
+ if ad && Dcc::TypeGuards.has_attribute?(ad,
88
+ :core_data)
89
+ ad.core_data
90
+ end
85
91
  end
86
92
 
87
93
  def items(dcc)
88
94
  ad = admin_data(dcc)
89
- return [] unless ad && Dcc::TypeGuards.has_attribute?(ad, :items) && ad.items && Dcc::TypeGuards.has_attribute?(ad.items, :item)
95
+ return [] unless ad && Dcc::TypeGuards.has_attribute?(ad,
96
+ :items) && ad.items && Dcc::TypeGuards.has_attribute?(
97
+ ad.items, :item
98
+ )
90
99
 
91
100
  Array(ad.items.item)
92
101
  end
93
102
 
94
103
  def measurement_results(dcc)
95
- Dcc::TypeGuards.has_attribute?(dcc, :measurement_results) && dcc.measurement_results && Dcc::TypeGuards.has_attribute?(dcc.measurement_results, :measurement_result) ?
96
- Array(dcc.measurement_results.measurement_result) : []
104
+ if Dcc::TypeGuards.has_attribute?(dcc,
105
+ :measurement_results) && dcc.measurement_results && Dcc::TypeGuards.has_attribute?(
106
+ dcc.measurement_results, :measurement_result
107
+ )
108
+ Array(dcc.measurement_results.measurement_result)
109
+ else
110
+ []
111
+ end
97
112
  end
98
113
 
99
114
  def count_quantities(mr_list)
100
115
  mr_list.sum do |mr|
101
- next 0 unless Dcc::TypeGuards.has_attribute?(mr, :results) && mr.results && Dcc::TypeGuards.has_attribute?(mr.results, :result)
116
+ next 0 unless Dcc::TypeGuards.has_attribute?(mr,
117
+ :results) && mr.results && Dcc::TypeGuards.has_attribute?(
118
+ mr.results, :result
119
+ )
102
120
 
103
121
  Array(mr.results.result).sum do |r|
104
- Dcc::TypeGuards.has_attribute?(r, :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data, :quantity) ? Array(r.data.quantity).size : 0
122
+ if Dcc::TypeGuards.has_attribute?(r,
123
+ :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data,
124
+ :quantity)
125
+ Array(r.data.quantity).size
126
+ else
127
+ 0
128
+ end
105
129
  end
106
130
  end
107
131
  end
108
132
 
109
133
  def count_lists(mr_list)
110
134
  mr_list.sum do |mr|
111
- next 0 unless Dcc::TypeGuards.has_attribute?(mr, :results) && mr.results && Dcc::TypeGuards.has_attribute?(mr.results, :result)
135
+ next 0 unless Dcc::TypeGuards.has_attribute?(mr,
136
+ :results) && mr.results && Dcc::TypeGuards.has_attribute?(
137
+ mr.results, :result
138
+ )
112
139
 
113
140
  Array(mr.results.result).sum do |r|
114
- Dcc::TypeGuards.has_attribute?(r, :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data, :list) ? Array(r.data.list).size : 0
141
+ if Dcc::TypeGuards.has_attribute?(r,
142
+ :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data,
143
+ :list)
144
+ Array(r.data.list).size
145
+ else
146
+ 0
147
+ end
115
148
  end
116
149
  end
117
150
  end
@@ -120,12 +153,16 @@ module Dcc
120
153
  cd = core_data(dcc)
121
154
  return [] unless cd
122
155
 
123
- [cd.used_lang_code_iso_639_1, cd.mandatory_lang_code_iso_639_1].flatten.compact.map(&:to_s)
156
+ [cd.used_lang_code_iso_639_1,
157
+ cd.mandatory_lang_code_iso_639_1].flatten.compact.map(&:to_s)
124
158
  end
125
159
 
126
160
  def resp_persons(dcc)
127
161
  ad = admin_data(dcc)
128
- return [] unless ad && Dcc::TypeGuards.has_attribute?(ad, :resp_persons) && ad.resp_persons && Dcc::TypeGuards.has_attribute?(ad.resp_persons, :resp_person)
162
+ return [] unless ad && Dcc::TypeGuards.has_attribute?(ad,
163
+ :resp_persons) && ad.resp_persons && Dcc::TypeGuards.has_attribute?(
164
+ ad.resp_persons, :resp_person
165
+ )
129
166
 
130
167
  Array(ad.resp_persons.resp_person)
131
168
  end
@@ -136,4 +173,4 @@ module Dcc
136
173
  end
137
174
  end
138
175
  end
139
- end
176
+ end
data/lib/dcc/inspect.rb CHANGED
@@ -6,4 +6,4 @@ module Dcc
6
6
  module Inspect
7
7
  autoload :Report, "dcc/inspect/report"
8
8
  end
9
- end
9
+ end