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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +19 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +560 -10
- data/CHANGELOG.adoc +34 -1
- data/CONTRIBUTING.adoc +8 -1
- data/Gemfile +5 -7
- data/README.adoc +162 -7
- data/dcc.gemspec +9 -2
- data/docs/schema-versions.adoc +20 -7
- data/examples/build_minimal_dcc.rb +0 -0
- data/examples/convert_all_formats.rb +2 -1
- data/examples/parse_and_inspect.rb +0 -0
- data/examples/quantity_math_demo.rb +7 -7
- data/examples/validate_full.rb +1 -1
- data/lib/dcc/base/calibration_laboratory.rb +2 -1
- data/lib/dcc/base/condition.rb +5 -2
- data/lib/dcc/base/core_data.rb +8 -3
- data/lib/dcc/base/formula.rb +3 -3
- data/lib/dcc/base/identification.rb +6 -3
- data/lib/dcc/base/mathml.rb +33 -0
- data/lib/dcc/base/quantity.rb +2 -1
- data/lib/dcc/base/resp_person.rb +4 -2
- data/lib/dcc/base/text.rb +1 -1
- data/lib/dcc/base.rb +2 -1
- data/lib/dcc/builder/dsl.rb +1 -1
- data/lib/dcc/builder/session.rb +16 -14
- data/lib/dcc/builder.rb +3 -3
- data/lib/dcc/cli/cli.rb +152 -26
- data/lib/dcc/cli/formatters.rb +35 -7
- data/lib/dcc/cli.rb +1 -1
- data/lib/dcc/context_configuration.rb +2 -1
- data/lib/dcc/context_options.rb +4 -2
- data/lib/dcc/convert/csv.rb +27 -9
- data/lib/dcc/convert/html.rb +26 -110
- data/lib/dcc/convert/json.rb +10 -7
- data/lib/dcc/convert/liquid_filters.rb +58 -0
- data/lib/dcc/convert/result.rb +1 -1
- data/lib/dcc/convert/templates/report.liquid +250 -0
- data/lib/dcc/convert/yaml.rb +1 -1
- data/lib/dcc/convert.rb +1 -0
- data/lib/dcc/diff/change.rb +1 -1
- data/lib/dcc/diff/result.rb +8 -4
- data/lib/dcc/diff.rb +43 -11
- data/lib/dcc/error.rb +7 -0
- data/lib/dcc/extract/file.rb +3 -4
- data/lib/dcc/extract/formula/ast.rb +137 -0
- data/lib/dcc/extract/formula/evaluator.rb +357 -0
- data/lib/dcc/extract/formula/parser.rb +305 -0
- data/lib/dcc/extract/formula/quantity.rb +89 -0
- data/lib/dcc/extract/formula.rb +110 -0
- data/lib/dcc/extract/ring.rb +1 -1
- data/lib/dcc/extract.rb +1 -1
- data/lib/dcc/i18n/text_lookup.rb +8 -4
- data/lib/dcc/i18n.rb +1 -1
- data/lib/dcc/inspect/report.rb +51 -14
- data/lib/dcc/inspect.rb +1 -1
- data/lib/dcc/migrate/route.rb +44 -0
- data/lib/dcc/migrate/v2_to_v3.rb +445 -0
- data/lib/dcc/migrate.rb +130 -26
- data/lib/dcc/plugin/base.rb +64 -0
- data/lib/dcc/plugin/registry.rb +7 -2
- data/lib/dcc/plugin.rb +6 -2
- data/lib/dcc/quantity_format/formatter.rb +16 -10
- data/lib/dcc/quantity_format.rb +1 -1
- data/lib/dcc/quantity_math/complex.rb +252 -0
- data/lib/dcc/quantity_math/matrix.rb +217 -0
- data/lib/dcc/quantity_math/quantity.rb +1 -1
- data/lib/dcc/quantity_math/real.rb +6 -6
- data/lib/dcc/quantity_math.rb +31 -3
- data/lib/dcc/schema/version.rb +2 -2
- data/lib/dcc/server/app.html +282 -0
- data/lib/dcc/server/app.rb +171 -87
- data/lib/dcc/server/demo_cert.xml +244 -0
- data/lib/dcc/server/storage/entry.rb +1 -1
- data/lib/dcc/server/storage/memory.rb +3 -2
- data/lib/dcc/server/storage.rb +1 -1
- data/lib/dcc/server.rb +1 -1
- data/lib/dcc/si/base/complex.rb +1 -1
- data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
- data/lib/dcc/si/base/constant.rb +1 -1
- data/lib/dcc/si/base/coverage_interval.rb +36 -0
- data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_unc.rb +34 -0
- data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
- data/lib/dcc/si/base/hybrid.rb +3 -2
- data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
- data/lib/dcc/si/base/real.rb +13 -3
- data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
- data/lib/dcc/si/base/si_list.rb +3 -2
- data/lib/dcc/si/base/standard_mu.rb +1 -1
- data/lib/dcc/si/base.rb +5 -2
- data/lib/dcc/si/v1/coverage_interval.rb +8 -0
- data/lib/dcc/si/v1/expanded_unc.rb +1 -16
- data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v1.rb +5 -2
- data/lib/dcc/si/v2/coverage_interval.rb +8 -0
- data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
- data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
- data/lib/dcc/si/v2/expanded_unc.rb +8 -0
- data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v2.rb +8 -3
- data/lib/dcc/signature/result.rb +2 -2
- data/lib/dcc/signature/signer.rb +31 -18
- data/lib/dcc/signature/verifier.rb +23 -21
- data/lib/dcc/signature.rb +1 -1
- data/lib/dcc/streaming/reader.rb +246 -0
- data/lib/dcc/streaming.rb +79 -0
- data/lib/dcc/transform/result.rb +1 -1
- data/lib/dcc/transform/xslt.rb +22 -12
- data/lib/dcc/transform.rb +5 -4
- data/lib/dcc/type/base64_binary.rb +5 -3
- data/lib/dcc/type/decimal_xml_list.rb +99 -13
- data/lib/dcc/type/iso_country_code.rb +3 -2
- data/lib/dcc/type/iso_language_code.rb +3 -2
- data/lib/dcc/type/schema_version.rb +3 -2
- data/lib/dcc/type/si_unit.rb +3 -2
- data/lib/dcc/type_guards.rb +3 -3
- data/lib/dcc/v2/calibration_laboratory.rb +2 -1
- data/lib/dcc/v2/condition.rb +22 -1
- data/lib/dcc/v2/configuration.rb +1 -1
- data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v2/identification.rb +14 -1
- data/lib/dcc/v2/mathml.rb +8 -0
- data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v2/quantity.rb +4 -2
- data/lib/dcc/v2.rb +4 -2
- data/lib/dcc/v3/calibration_laboratory.rb +2 -1
- data/lib/dcc/v3/configuration.rb +1 -1
- data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v3/mathml.rb +8 -0
- data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v3/quantity.rb +4 -2
- data/lib/dcc/v3.rb +12 -2
- data/lib/dcc/validate/business_rules.rb +23 -9
- data/lib/dcc/validate/issue.rb +2 -1
- data/lib/dcc/validate/result.rb +6 -4
- data/lib/dcc/validate/schematron/profile.rb +73 -18
- data/lib/dcc/validate/schematron/rule.rb +13 -3
- data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
- data/lib/dcc/validate/schematron/rules/base.rb +1 -2
- data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
- data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
- data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
- data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
- data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
- data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
- data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
- data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
- data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
- data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
- data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
- data/lib/dcc/validate/schematron/rules.rb +28 -14
- data/lib/dcc/validate/schematron.rb +1 -1
- data/lib/dcc/validate/xsd.rb +9 -3
- data/lib/dcc/validate.rb +1 -1
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc/versioned_parser.rb +1 -1
- data/lib/dcc.rb +68 -10
- data/sample_dcc.xml +140 -0
- data/web/index.html +12 -0
- data/web/package-lock.json +1599 -0
- data/web/package.json +22 -0
- data/web/src/App.vue +349 -0
- data/web/src/lib/builder.ts +171 -0
- data/web/src/lib/dcc.ts +338 -0
- data/web/src/lib/units.json +5581 -0
- data/web/src/main.ts +5 -0
- data/web/src/style.css +27 -0
- data/web/tsconfig.json +15 -0
- data/web/vite.config.ts +14 -0
- metadata +148 -9
data/lib/dcc/error.rb
CHANGED
|
@@ -37,9 +37,16 @@ module Dcc
|
|
|
37
37
|
# Raised when an unsupported schema version is requested.
|
|
38
38
|
class UnknownVersionError < Error; end
|
|
39
39
|
|
|
40
|
+
# Raised when a migration direction is not supported — any downgrade, and
|
|
41
|
+
# any pair whose incompatibilities have not been verified.
|
|
42
|
+
class UnsupportedMigrationError < Error; end
|
|
43
|
+
|
|
40
44
|
# Raised when the builder DSL is used incorrectly.
|
|
41
45
|
class BuilderError < Error; end
|
|
42
46
|
|
|
47
|
+
# Raised when a plugin gem's entry file cannot be loaded.
|
|
48
|
+
class PluginError < Error; end
|
|
49
|
+
|
|
43
50
|
# Raised when a soft dependency (`xmldsig`, `sinatra`, etc.) is required
|
|
44
51
|
# but not installed.
|
|
45
52
|
class MissingDependencyError < Error
|
data/lib/dcc/extract/file.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "base64"
|
|
4
|
-
require "set"
|
|
5
4
|
|
|
6
5
|
module Dcc
|
|
7
6
|
module Extract
|
|
@@ -40,7 +39,7 @@ module Dcc
|
|
|
40
39
|
# @param dcc [Lutaml::Model::Serializable]
|
|
41
40
|
# @yieldparam [Dcc::Extract::File]
|
|
42
41
|
# @return [Array<Dcc::Extract::File>] the list when no block given.
|
|
43
|
-
def each(dcc, &
|
|
42
|
+
def each(dcc, &)
|
|
44
43
|
list = []
|
|
45
44
|
visited = ::Set.new
|
|
46
45
|
|
|
@@ -65,7 +64,7 @@ module Dcc
|
|
|
65
64
|
collect.call(dcc, nil)
|
|
66
65
|
return list unless block_given?
|
|
67
66
|
|
|
68
|
-
list.each(&
|
|
67
|
+
list.each(&)
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
# @param dcc [Lutaml::Model::Serializable]
|
|
@@ -140,4 +139,4 @@ module Dcc
|
|
|
140
139
|
end
|
|
141
140
|
end
|
|
142
141
|
end
|
|
143
|
-
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Dcc
|
|
6
|
+
module Extract
|
|
7
|
+
module Formula
|
|
8
|
+
# One extracted formula: its declared name, the lambda's bound
|
|
9
|
+
# variables, the expression tree, and the variable bindings found
|
|
10
|
+
# alongside it in the DCC.
|
|
11
|
+
class Ast
|
|
12
|
+
attr_reader :name, :bound_variables, :body, :bindings
|
|
13
|
+
|
|
14
|
+
# @param name [String, nil] the `ml:declare` function name (e.g. "R").
|
|
15
|
+
# @param bound_variables [Array<String>] the `ml:lambda` bvar names.
|
|
16
|
+
# @param body [Object] the expression tree root.
|
|
17
|
+
# @param bindings [Hash{String => Quantity}] variable values
|
|
18
|
+
# found in the DCC.
|
|
19
|
+
def initialize(name:, bound_variables:, body:, bindings:)
|
|
20
|
+
@name = name
|
|
21
|
+
@bound_variables = bound_variables
|
|
22
|
+
@body = body
|
|
23
|
+
@bindings = bindings
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param overrides [Hash{String => BigDecimal, Array<BigDecimal>}]
|
|
27
|
+
# @return [Array<BigDecimal>]
|
|
28
|
+
def evaluate(overrides = {})
|
|
29
|
+
Evaluator.call(self, overrides)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Bound and free variables are both reported: an override may
|
|
33
|
+
# legitimately name either. Repeats are left in — the evaluator
|
|
34
|
+
# slices with these names and the CLI subtracts them, and neither
|
|
35
|
+
# cares how often a name appears.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<String>] every variable the body mentions.
|
|
38
|
+
def variables
|
|
39
|
+
names_in(body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
"#{name || '(anonymous)'}(#{bound_variables.join(', ')})"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `Formula.call` hands `Ast` objects back as public API, so callers
|
|
47
|
+
# legitimately build and hold nodes. Number therefore coerces
|
|
48
|
+
# through the same boundary as Quantity.
|
|
49
|
+
Number = ::Data.define(:value) do
|
|
50
|
+
def initialize(value:)
|
|
51
|
+
super(value: Quantity.decimal(value))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Variable = ::Data.define(:name)
|
|
56
|
+
BoundVariable = ::Data.define(:name)
|
|
57
|
+
|
|
58
|
+
# The symbols the evaluator can dispatch on. Its `case` statements
|
|
59
|
+
# have no `else`, so an unrecognised name would evaluate to nil —
|
|
60
|
+
# a silent wrong answer for a constant, a NoMethodError for an
|
|
61
|
+
# operator. Rejecting at construction keeps both out of the API.
|
|
62
|
+
CONSTANTS = %i[pi e].freeze
|
|
63
|
+
OPERATORS = %i[+ - * / ** root exp ln log sin cos tan abs].freeze
|
|
64
|
+
|
|
65
|
+
Constant = ::Data.define(:name) do
|
|
66
|
+
def initialize(name:)
|
|
67
|
+
Constant.check_name(name)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.check_name(name)
|
|
72
|
+
return if CONSTANTS.include?(name)
|
|
73
|
+
|
|
74
|
+
raise ::Dcc::ExtractionError,
|
|
75
|
+
"#{name} is not a formula constant, expected one of " \
|
|
76
|
+
"#{CONSTANTS.join(', ')}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# How many operands each operator accepts; anything absent is a
|
|
81
|
+
# single-argument function. Without this, `<power/>` with one
|
|
82
|
+
# operand quietly computes `x ** x` and with three quietly drops
|
|
83
|
+
# the middle one — wrong answers rather than errors.
|
|
84
|
+
ARITY = {
|
|
85
|
+
"+": (1..),
|
|
86
|
+
"*": (1..),
|
|
87
|
+
"-": (1..2),
|
|
88
|
+
"/": (2..2),
|
|
89
|
+
"**": (2..2),
|
|
90
|
+
root: (1..2),
|
|
91
|
+
}.freeze
|
|
92
|
+
|
|
93
|
+
UNARY = (1..1)
|
|
94
|
+
|
|
95
|
+
# Checked in the constructor so a directly-built AST is held to the
|
|
96
|
+
# same contract as a parsed one.
|
|
97
|
+
Apply = ::Data.define(:operator, :operands) do
|
|
98
|
+
def initialize(operator:, operands:)
|
|
99
|
+
Apply.check_operator(operator)
|
|
100
|
+
Apply.check_arity(operator, operands)
|
|
101
|
+
super(operator: operator, operands: operands.dup.freeze)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.check_operator(operator)
|
|
105
|
+
return if OPERATORS.include?(operator)
|
|
106
|
+
|
|
107
|
+
raise ::Dcc::ExtractionError,
|
|
108
|
+
"#{operator} is not a formula operator, expected one of " \
|
|
109
|
+
"#{OPERATORS.join(', ')}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.check_arity(operator, operands)
|
|
113
|
+
allowed = ARITY.fetch(operator, UNARY)
|
|
114
|
+
return if allowed.cover?(operands.size)
|
|
115
|
+
|
|
116
|
+
expected = allowed.end ? allowed.to_s : "#{allowed.begin} or more"
|
|
117
|
+
raise ::Dcc::ExtractionError,
|
|
118
|
+
"#{operator} takes #{expected} operand(s) in formula, " \
|
|
119
|
+
"got #{operands.size}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Walks the tree for `variables`. Private: the tree shape is not
|
|
126
|
+
# part of what this gem promises.
|
|
127
|
+
def names_in(node)
|
|
128
|
+
case node
|
|
129
|
+
in (Variable | BoundVariable) => ref then [ref.name]
|
|
130
|
+
in Apply(operands:) then operands.flat_map { |o| names_in(o) }
|
|
131
|
+
else []
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "bigdecimal/math"
|
|
5
|
+
|
|
6
|
+
module Dcc
|
|
7
|
+
module Extract
|
|
8
|
+
module Formula
|
|
9
|
+
# Evaluates a `Formula::Ast` numerically.
|
|
10
|
+
#
|
|
11
|
+
# Only variables the formula actually references take part:
|
|
12
|
+
# extraction hands over every identified quantity in the data block,
|
|
13
|
+
# and letting an unreferenced sibling list drive the result width
|
|
14
|
+
# would fail valid formulae. Referenced list variables must agree on
|
|
15
|
+
# length and combine element-wise, so the result is always an array.
|
|
16
|
+
#
|
|
17
|
+
# PRECISION applies to inexact operations only — `/`, `root`, `exp`,
|
|
18
|
+
# `ln`, `log`, the trig functions, non-integer `**` and negative
|
|
19
|
+
# integer `**`. Addition, subtraction, multiplication and
|
|
20
|
+
# non-negative integer powers stay exact.
|
|
21
|
+
#
|
|
22
|
+
# Only the long-stable `BigMath` subset is used (`sqrt`, `exp`,
|
|
23
|
+
# `log`, `sin`, `cos`, `PI`, `E`) — `BigMath.tan` and
|
|
24
|
+
# `BigMath.log10` are recent additions and `bigdecimal` ships as a
|
|
25
|
+
# bundled gem from Ruby 3.4, so the version we get is not ours to
|
|
26
|
+
# assume.
|
|
27
|
+
module Evaluator
|
|
28
|
+
PRECISION = 34
|
|
29
|
+
TEN = BigDecimal(10)
|
|
30
|
+
ZERO_DIVISION = "Division by zero in formula"
|
|
31
|
+
|
|
32
|
+
# A cost ceiling, not a correctness one. Three things in here
|
|
33
|
+
# scale with the size of a value, and `BigDecimal#precision`
|
|
34
|
+
# counts digits across the exponent span, so one limit covers
|
|
35
|
+
# all three:
|
|
36
|
+
#
|
|
37
|
+
# * BigMath reduces a trigonometric argument before it can
|
|
38
|
+
# answer, and that is linear in the exponent — sin(1e1000)
|
|
39
|
+
# is 1ms, sin(1e100000) 0.74s, sin(1e1000000) 8.6s.
|
|
40
|
+
# * exact `**` and `*` allocate one digit at a time, and the
|
|
41
|
+
# exponent does not constrain that at all: 0.99999999999
|
|
42
|
+
# raised to 999 twice keeps exponent 0 while going from 11
|
|
43
|
+
# digits to eleven million.
|
|
44
|
+
# * the printed value — `to_s("F")` of 1e1000000 is a megabyte.
|
|
45
|
+
#
|
|
46
|
+
# Nothing real comes near it: exp(1000) is 435, tan(pi/2) is 34,
|
|
47
|
+
# 2**1000 is 302, and IEEE double tops out at 1e308.
|
|
48
|
+
#
|
|
49
|
+
# `precision` has been on BigDecimal since 3.0.0, well under the
|
|
50
|
+
# gem's Ruby floor of 3.2.
|
|
51
|
+
MAX_DIGITS = 1_000
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# @param ast [Dcc::Extract::Formula::Ast]
|
|
55
|
+
# @param overrides [Hash{String => Object}]
|
|
56
|
+
# @return [Array<BigDecimal>]
|
|
57
|
+
def call(ast, overrides = {})
|
|
58
|
+
env = environment(ast, overrides)
|
|
59
|
+
Array.new(width(env)) do |index|
|
|
60
|
+
evaluate(ast.body, scope(env, index))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Values stay wrapped in a Quantity through the whole environment
|
|
67
|
+
# so `list?` survives. Flattening to a bare array here would make
|
|
68
|
+
# a genuine one-element list indistinguishable from a scalar, and
|
|
69
|
+
# it would then broadcast against a longer list instead of
|
|
70
|
+
# raising a length mismatch.
|
|
71
|
+
def environment(ast, overrides)
|
|
72
|
+
ast.bindings.merge(overrides)
|
|
73
|
+
.slice(*ast.variables)
|
|
74
|
+
.to_h { |name, value| [name, sized(name, quantify(value))] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Only the referenced slice is checked. `Formula.bindings_in`
|
|
78
|
+
# builds a Quantity for every identified quantity in the data
|
|
79
|
+
# block before it looks at the formulae, so refusing an
|
|
80
|
+
# oversized one at coercion would take down every other
|
|
81
|
+
# formula in the document over a value none of them uses.
|
|
82
|
+
def sized(name, quantity)
|
|
83
|
+
subject = "Formula variable '#{name}'"
|
|
84
|
+
numbers = quantity.values
|
|
85
|
+
numbers.each { |value| check_size(value, subject) }
|
|
86
|
+
quantity
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A Quantity coerced every value in its constructor, so its
|
|
90
|
+
# contents are already finite BigDecimals. A raw override has had
|
|
91
|
+
# no such treatment, so it goes through the same constructor.
|
|
92
|
+
def quantify(value)
|
|
93
|
+
return value if value.is_a?(Quantity)
|
|
94
|
+
|
|
95
|
+
Quantity.new(value: value)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Only list variables set the width; scalars broadcast. Every
|
|
99
|
+
# list must agree, whatever its length — a one-element list is
|
|
100
|
+
# still a list.
|
|
101
|
+
def width(env)
|
|
102
|
+
lists = env.select { |_, quantity| quantity.list? }
|
|
103
|
+
distinct = lists.each_value.map { |q| q.values.size }.uniq
|
|
104
|
+
check_lengths(distinct, lists.keys)
|
|
105
|
+
|
|
106
|
+
distinct.first || 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Refuses both ways a referenced list can make a width
|
|
110
|
+
# meaningless. An empty one used to give width zero, so
|
|
111
|
+
# `Array.new(0)` never evaluated the body at all: no values,
|
|
112
|
+
# no error, exit 0 — and a genuinely missing variable went
|
|
113
|
+
# unreported with it, because nothing ever looked one up.
|
|
114
|
+
def check_lengths(distinct, names)
|
|
115
|
+
if distinct.size > 1
|
|
116
|
+
raise ::Dcc::ExtractionError,
|
|
117
|
+
"Formula variables have mismatched list lengths: " \
|
|
118
|
+
"#{distinct.sort.inspect}"
|
|
119
|
+
end
|
|
120
|
+
return unless distinct == [0]
|
|
121
|
+
|
|
122
|
+
raise ::Dcc::ExtractionError,
|
|
123
|
+
"Formula variables have empty value lists: " \
|
|
124
|
+
"#{names.sort.inspect}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scope(env, index)
|
|
128
|
+
env.transform_values do |quantity|
|
|
129
|
+
quantity.list? ? quantity.values[index] : quantity.value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Bound and free variables resolve identically; the distinction
|
|
134
|
+
# matters to the parser, not here. They share one clause via
|
|
135
|
+
# `(A | B) => ref`, which binds the node itself — `A(name:) |
|
|
136
|
+
# B(name:)` is a syntax error ("duplicated variable name").
|
|
137
|
+
def evaluate(node, scope)
|
|
138
|
+
case node
|
|
139
|
+
in Ast::Number(value:) then check_size(value, "A formula number")
|
|
140
|
+
in Ast::Constant(name:) then constant(name)
|
|
141
|
+
in (Ast::Variable | Ast::BoundVariable) => ref
|
|
142
|
+
lookup(ref.name, scope)
|
|
143
|
+
in Ast::Apply(operator:, operands:)
|
|
144
|
+
operate(operator, operands.map { |o| evaluate(o, scope) })
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Needs no size check: BigMath.PI(34) and E(34) are 34 digits
|
|
149
|
+
# by construction, so a constant cannot reach the ceiling.
|
|
150
|
+
def constant(name)
|
|
151
|
+
case name
|
|
152
|
+
when :pi then BigMath.PI(PRECISION)
|
|
153
|
+
when :e then BigMath.E(PRECISION)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def lookup(name, scope)
|
|
158
|
+
scope.fetch(name) do
|
|
159
|
+
raise ::Dcc::ExtractionError,
|
|
160
|
+
"No value for formula variable '#{name}'"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Every computed value is checked before it can become another
|
|
165
|
+
# operand. Guarding inputs alone is not enough: `exp(1e100)` is
|
|
166
|
+
# Infinity and `sin(Infinity)` is NaN from finite inputs, and a
|
|
167
|
+
# non-finite operand then poisons everything above it. A huge but
|
|
168
|
+
# finite result still passes.
|
|
169
|
+
def operate(operator, args)
|
|
170
|
+
checked(dispatch(operator, args), operator)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Every value the evaluator produces goes through here — the
|
|
174
|
+
# answer of an operation and every intermediate inside an
|
|
175
|
+
# n-ary fold alike. Checking only the answer let
|
|
176
|
+
# 1.25e998 * 1.25e998 * 8e-999 * 8e-999 run the accumulator up
|
|
177
|
+
# to an exponent of 1997 and land back on exactly 1 unnoticed.
|
|
178
|
+
def checked(value, operator, role = "result of")
|
|
179
|
+
check_size(finite(value, operator), "The #{role} #{operator}")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def check_size(value, subject)
|
|
183
|
+
return value if value.precision <= MAX_DIGITS
|
|
184
|
+
|
|
185
|
+
raise ::Dcc::ExtractionError,
|
|
186
|
+
"#{subject} exceeds the formula limit of " \
|
|
187
|
+
"#{MAX_DIGITS} digits"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def finite(result, operator)
|
|
191
|
+
return result if result.finite?
|
|
192
|
+
|
|
193
|
+
raise ::Dcc::ExtractionError,
|
|
194
|
+
"#{operator} produced a non-finite result in formula"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Split by shape: n-ary algebraic operators fold over their
|
|
198
|
+
# operands, the rest are single-argument functions.
|
|
199
|
+
def dispatch(operator, args)
|
|
200
|
+
case operator
|
|
201
|
+
when :+, :*, :-, :/ then fold(operator, args)
|
|
202
|
+
when :** then power(args.first, args.last)
|
|
203
|
+
when :root then root(args.first, args[1])
|
|
204
|
+
else function(operator, args.first)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Folds through `checked` so an intermediate cannot exceed the
|
|
209
|
+
# ceiling on its way to an answer that does not.
|
|
210
|
+
#
|
|
211
|
+
# `Array#sum` seeds with 0 and that seed is observable —
|
|
212
|
+
# `[-0].sum` is 0.0 where `[-0].reduce(:+)` is -0.0 — so `+`
|
|
213
|
+
# folds from the same seed and no printed value moves.
|
|
214
|
+
def fold(operator, args)
|
|
215
|
+
return -args.first if operator == :- && args.size == 1
|
|
216
|
+
|
|
217
|
+
operands = operator == :+ ? [BigDecimal(0), *args] : args
|
|
218
|
+
operands.reduce do |left, right|
|
|
219
|
+
checked(step(operator, left, right), operator)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def step(operator, left, right)
|
|
224
|
+
case operator
|
|
225
|
+
when :+ then left + right
|
|
226
|
+
when :* then left * right
|
|
227
|
+
when :- then left - right
|
|
228
|
+
else divide(left, right)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def function(operator, arg)
|
|
233
|
+
case operator
|
|
234
|
+
when :exp then BigMath.exp(arg, PRECISION)
|
|
235
|
+
when :ln then ln(arg)
|
|
236
|
+
when :log then divide(ln(arg), ln(TEN))
|
|
237
|
+
when :abs then arg.abs
|
|
238
|
+
else trigonometric(operator, arg)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def trigonometric(operator, arg)
|
|
243
|
+
case operator
|
|
244
|
+
when :sin then BigMath.sin(arg, PRECISION)
|
|
245
|
+
when :cos then BigMath.cos(arg, PRECISION)
|
|
246
|
+
when :tan then tan(arg)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# BigDecimal returns Infinity/NaN for a zero divisor rather than
|
|
251
|
+
# raising, so guard the input.
|
|
252
|
+
def divide(numerator, denominator)
|
|
253
|
+
raise ::Dcc::ExtractionError, ZERO_DIVISION if denominator.zero?
|
|
254
|
+
|
|
255
|
+
numerator.div(denominator, PRECISION)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def tan(value)
|
|
259
|
+
divide(BigMath.sin(value, PRECISION),
|
|
260
|
+
BigMath.cos(value, PRECISION))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def ln(value)
|
|
264
|
+
if value <= 0
|
|
265
|
+
raise ::Dcc::ExtractionError,
|
|
266
|
+
"Logarithm of a non-positive value in formula"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
BigMath.log(value, PRECISION)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Non-negative integer exponents stay exact; everything else goes
|
|
273
|
+
# through the explicit-precision path. `0 ** 0.5` is 0, not an
|
|
274
|
+
# error, so zero is handled before the ln guard can reject it.
|
|
275
|
+
def power(base, exponent)
|
|
276
|
+
return integer_power(base, exponent.to_i) if exponent.frac.zero?
|
|
277
|
+
return zero_to_the(exponent) if base.zero?
|
|
278
|
+
if base.negative?
|
|
279
|
+
raise ::Dcc::ExtractionError,
|
|
280
|
+
"Cannot raise a negative base to a fractional power " \
|
|
281
|
+
"in formula"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
scaled = checked(exponent * ln(base), :**, "intermediate in")
|
|
285
|
+
BigMath.exp(scaled, PRECISION)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# `**` chooses its own precision, so a negative exponent is
|
|
289
|
+
# inexact (`3 ** -1` gives 32 digits, not 34). Route it through
|
|
290
|
+
# `divide`.
|
|
291
|
+
def integer_power(base, exponent)
|
|
292
|
+
check_power_size(base, exponent)
|
|
293
|
+
return base**exponent unless exponent.negative?
|
|
294
|
+
raise ::Dcc::ExtractionError, ZERO_DIVISION if base.zero?
|
|
295
|
+
|
|
296
|
+
divide(BigDecimal(1), base**-exponent)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The exact path allocates the whole number before any result
|
|
300
|
+
# check can see how big it got, so the size is predicted here
|
|
301
|
+
# instead. `2 ** 1e8` takes 2.7s and `2 ** 1e9` 25s, and
|
|
302
|
+
# `(0.99999999999 ** 999) ** 999` needs eleven million digits
|
|
303
|
+
# while its exponent never leaves zero.
|
|
304
|
+
#
|
|
305
|
+
# `precision * |exponent|` is an upper bound, not the exact
|
|
306
|
+
# size, so it refuses some powers that would have fitted:
|
|
307
|
+
# anything with an integer exponent past MAX_DIGITS, however
|
|
308
|
+
# small the base (`2 ** 1001` really lands on 302 digits), and
|
|
309
|
+
# `1 ** huge`, which costs nothing. Predicting exactly means
|
|
310
|
+
# hand-rolling capped exponentiation, and a subtle bug in that
|
|
311
|
+
# loop fails this guard open. An over-refusal is a message; an
|
|
312
|
+
# under-refusal is the CPU-exhaustion path this exists to
|
|
313
|
+
# close. No certificate raises anything to the 1001st power.
|
|
314
|
+
def check_power_size(base, exponent)
|
|
315
|
+
return if base.precision * exponent.abs <= MAX_DIGITS
|
|
316
|
+
|
|
317
|
+
raise ::Dcc::ExtractionError,
|
|
318
|
+
"An exact power in the formula is not provably within " \
|
|
319
|
+
"the limit of #{MAX_DIGITS} digits"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def zero_to_the(exponent)
|
|
323
|
+
raise ::Dcc::ExtractionError, ZERO_DIVISION if exponent.negative?
|
|
324
|
+
|
|
325
|
+
BigDecimal(0)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Content MathML spells roots as `<root/>` with an optional
|
|
329
|
+
# `<degree>`; mml exposes no content `sqrt` element. Odd-degree
|
|
330
|
+
# roots of negative values are out of scope — the exp/ln path
|
|
331
|
+
# cannot produce them. A zero radicand is legitimate and is
|
|
332
|
+
# answered before the ln guard sees it.
|
|
333
|
+
def root(value, degree)
|
|
334
|
+
if value.negative?
|
|
335
|
+
raise ::Dcc::ExtractionError,
|
|
336
|
+
"Cannot take the root of a negative value in formula"
|
|
337
|
+
end
|
|
338
|
+
return BigMath.sqrt(value, PRECISION) if degree.nil? || degree == 2
|
|
339
|
+
|
|
340
|
+
nth_root(value, degree)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def nth_root(value, degree)
|
|
344
|
+
if degree.zero?
|
|
345
|
+
raise ::Dcc::ExtractionError, "Root of degree zero in formula"
|
|
346
|
+
end
|
|
347
|
+
return zero_to_the(degree) if value.zero?
|
|
348
|
+
|
|
349
|
+
scaled = checked(divide(ln(value), degree), :root,
|
|
350
|
+
"intermediate in")
|
|
351
|
+
BigMath.exp(scaled, PRECISION)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|