fhirpath 0.2.0.pre1
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 +7 -0
- data/.rubocop.yml +73 -0
- data/CHANGELOG.md +77 -0
- data/CONTRIBUTING.md +67 -0
- data/Gemfile +12 -0
- data/LICENSE +21 -0
- data/README.md +218 -0
- data/Rakefile +19 -0
- data/SECURITY.md +20 -0
- data/conformance/core.jsonl +39 -0
- data/conformance/official-r4-core.json +15 -0
- data/conformance/r4.jsonl +6 -0
- data/docs/api.md +170 -0
- data/docs/architecture.md +507 -0
- data/docs/conformance.md +67 -0
- data/docs/feature-matrix.md +49 -0
- data/docs/first-slice.md +57 -0
- data/docs/release-checklist.md +57 -0
- data/docs/releasing.md +87 -0
- data/docs/support-matrix.md +102 -0
- data/lib/fhirpath/ast.rb +128 -0
- data/lib/fhirpath/capability.rb +57 -0
- data/lib/fhirpath/collection.rb +94 -0
- data/lib/fhirpath/compiled_expression.rb +35 -0
- data/lib/fhirpath/conformance/importer.rb +317 -0
- data/lib/fhirpath/errors.rb +76 -0
- data/lib/fhirpath/evaluation_context.rb +30 -0
- data/lib/fhirpath/evaluator.rb +690 -0
- data/lib/fhirpath/functions.rb +71 -0
- data/lib/fhirpath/host_services.rb +52 -0
- data/lib/fhirpath/model.rb +47 -0
- data/lib/fhirpath/model_registry.rb +17 -0
- data/lib/fhirpath/models_r4.rb +91 -0
- data/lib/fhirpath/parser.rb +487 -0
- data/lib/fhirpath/source_span.rb +25 -0
- data/lib/fhirpath/types.rb +73 -0
- data/lib/fhirpath/vector_runner.rb +130 -0
- data/lib/fhirpath/version.rb +9 -0
- data/lib/fhirpath.rb +77 -0
- data/script/import_vectors.rb +14 -0
- data/script/run_vectors.rb +19 -0
- metadata +120 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module FHIRPath
|
|
6
|
+
class Evaluator
|
|
7
|
+
def evaluate(node, context)
|
|
8
|
+
evaluate_node(node, context)
|
|
9
|
+
rescue Error => e
|
|
10
|
+
raise e if e.expression
|
|
11
|
+
|
|
12
|
+
raise e.class.new(
|
|
13
|
+
e.message,
|
|
14
|
+
code: e.code,
|
|
15
|
+
span: e.span,
|
|
16
|
+
expression: context.options[:expression],
|
|
17
|
+
cause: e.original_cause
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def evaluate_node(node, context)
|
|
24
|
+
case node
|
|
25
|
+
when AST::Literal
|
|
26
|
+
Collection.new([node.value])
|
|
27
|
+
when AST::CollectionLiteral
|
|
28
|
+
Collection.new(node.elements.flat_map { |element| evaluate(element, context).items })
|
|
29
|
+
when AST::Identifier
|
|
30
|
+
identifier(node, context)
|
|
31
|
+
when AST::Variable
|
|
32
|
+
variable(node, context)
|
|
33
|
+
when AST::ExternalConstant
|
|
34
|
+
external_constant(node, context)
|
|
35
|
+
when AST::MemberInvocation
|
|
36
|
+
navigate(node, context)
|
|
37
|
+
when AST::Indexer
|
|
38
|
+
index(node, context)
|
|
39
|
+
when AST::FunctionInvocation
|
|
40
|
+
function(node, context)
|
|
41
|
+
when AST::UnaryExpression
|
|
42
|
+
unary(node, context)
|
|
43
|
+
when AST::BinaryExpression
|
|
44
|
+
binary(node, context)
|
|
45
|
+
else
|
|
46
|
+
raise EvaluationError.new("unsupported AST node: #{node.class}", code: :unsupported_ast)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def variable(node, context)
|
|
51
|
+
value = case node.name
|
|
52
|
+
when 'this'
|
|
53
|
+
context.focus
|
|
54
|
+
when 'index'
|
|
55
|
+
context.index.nil? ? Collection.empty : Collection.new([context.index])
|
|
56
|
+
when 'total'
|
|
57
|
+
context.total.nil? ? Collection.empty : Collection.new([context.total])
|
|
58
|
+
else
|
|
59
|
+
unless context.variables.key?(node.name)
|
|
60
|
+
raise EvaluationError.new("unknown variable: $#{node.name}",
|
|
61
|
+
code: :unknown_variable, span: node.span)
|
|
62
|
+
end
|
|
63
|
+
Collection.from(context.variables[node.name])
|
|
64
|
+
end
|
|
65
|
+
Collection.from(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def external_constant(node, context)
|
|
69
|
+
value = if context.variables.key?(node.name)
|
|
70
|
+
context.variables[node.name]
|
|
71
|
+
else
|
|
72
|
+
context.host.constant(node.name, context: context)
|
|
73
|
+
end
|
|
74
|
+
Collection.from(value)
|
|
75
|
+
rescue UnknownConstantError, HostError => e
|
|
76
|
+
raise e.class.new(
|
|
77
|
+
e.message,
|
|
78
|
+
code: e.code,
|
|
79
|
+
span: node.span,
|
|
80
|
+
cause: e.original_cause
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def identifier(node, context)
|
|
85
|
+
values = []
|
|
86
|
+
model_types = []
|
|
87
|
+
context.focus.items.each do |item|
|
|
88
|
+
if context.model.root_type(item).to_s == node.name
|
|
89
|
+
values << item
|
|
90
|
+
model_types << nil
|
|
91
|
+
else
|
|
92
|
+
append_property(values, model_types, item, node.name, context)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
Collection.new(values, types: model_types)
|
|
96
|
+
rescue NoMethodError => e
|
|
97
|
+
raise ModelError.new("cannot read #{node.name}", code: :model_navigation,
|
|
98
|
+
span: node.span, cause: e)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def navigate(node, context)
|
|
102
|
+
receiver = evaluate(node.receiver, context)
|
|
103
|
+
values = []
|
|
104
|
+
model_types = []
|
|
105
|
+
receiver.items.each do |item|
|
|
106
|
+
append_property(values, model_types, item, node.name, context)
|
|
107
|
+
end
|
|
108
|
+
Collection.new(values, types: model_types)
|
|
109
|
+
rescue NoMethodError => e
|
|
110
|
+
raise ModelError.new("cannot read #{node.name}", code: :model_navigation,
|
|
111
|
+
span: node.span, cause: e)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Resolves one property and records, per produced item, the logical type
|
|
115
|
+
# name the model declares for the resolved value (nil when the model has no
|
|
116
|
+
# metadata for this property, e.g. PlainModel or a non-choice field).
|
|
117
|
+
def append_property(values, model_types, item, logical_name, context)
|
|
118
|
+
produced = Collection.from(context.model.property(item, logical_name)).items
|
|
119
|
+
type = context.model.property_logical_type(item, logical_name)
|
|
120
|
+
values.concat(produced)
|
|
121
|
+
model_types.concat(Array.new(produced.length, type))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def index(node, context)
|
|
125
|
+
values = evaluate(node.receiver, context)
|
|
126
|
+
return Collection.empty if values.empty?
|
|
127
|
+
|
|
128
|
+
index_values = evaluate(node.index, context)
|
|
129
|
+
return Collection.empty if index_values.empty?
|
|
130
|
+
|
|
131
|
+
position = require_singleton(index_values, node.index.span)
|
|
132
|
+
unless position.is_a?(::Integer)
|
|
133
|
+
raise TypeError.new('index must be an integer', code: :expected_integer_index,
|
|
134
|
+
span: node.index.span)
|
|
135
|
+
end
|
|
136
|
+
return Collection.empty if position.negative? || position >= values.count
|
|
137
|
+
|
|
138
|
+
Collection.new([values.items[position]])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def function(node, context)
|
|
142
|
+
spec = context.functions.find(node.name)
|
|
143
|
+
validate_function!(node, spec)
|
|
144
|
+
receiver = node.receiver ? evaluate(node.receiver, context) : context.focus
|
|
145
|
+
invoke_function(node, receiver, context, spec)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_function!(node, spec)
|
|
149
|
+
unless spec
|
|
150
|
+
raise UnknownFunctionError.new(
|
|
151
|
+
"unknown function: #{node.name}", code: :unknown_function, span: node.span
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
return if valid_arity?(spec.arity, node.arguments.length)
|
|
155
|
+
|
|
156
|
+
raise EvaluationError.new(
|
|
157
|
+
"wrong number of arguments for #{node.name}", code: :invalid_arity, span: node.span
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def invoke_function(node, receiver, context, spec)
|
|
162
|
+
case node.name
|
|
163
|
+
when 'where'
|
|
164
|
+
filter(receiver, node.arguments.first, context)
|
|
165
|
+
when 'select'
|
|
166
|
+
select(receiver, node.arguments.first, context)
|
|
167
|
+
when 'first'
|
|
168
|
+
receiver.empty? ? Collection.empty : Collection.new([receiver.first_item])
|
|
169
|
+
when 'last'
|
|
170
|
+
receiver.empty? ? Collection.empty : Collection.new([receiver.items.last])
|
|
171
|
+
when 'tail'
|
|
172
|
+
receiver.empty? ? Collection.empty : Collection.new(receiver.items[1..])
|
|
173
|
+
when 'take'
|
|
174
|
+
take(receiver, integer_argument(node, context))
|
|
175
|
+
when 'skip'
|
|
176
|
+
skip(receiver, integer_argument(node, context))
|
|
177
|
+
when 'exists'
|
|
178
|
+
exists(receiver, node.arguments.first, context)
|
|
179
|
+
when 'count'
|
|
180
|
+
Collection.new([receiver.count])
|
|
181
|
+
when 'sum'
|
|
182
|
+
sum(receiver, node)
|
|
183
|
+
when 'avg'
|
|
184
|
+
average(receiver, node)
|
|
185
|
+
when 'max', 'min'
|
|
186
|
+
extremum(receiver, node.name, node)
|
|
187
|
+
when 'empty'
|
|
188
|
+
Collection.new([receiver.empty?])
|
|
189
|
+
when 'not'
|
|
190
|
+
negate(receiver, node)
|
|
191
|
+
when 'all'
|
|
192
|
+
all(receiver, node.arguments.first, context)
|
|
193
|
+
when 'allTrue', 'anyTrue', 'allFalse', 'anyFalse'
|
|
194
|
+
boolean_aggregate(receiver, node.name, node)
|
|
195
|
+
else
|
|
196
|
+
invoke_registered(node, receiver, context, spec)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def exists(receiver, expression, context)
|
|
201
|
+
return Collection.new([!receiver.empty?]) unless expression
|
|
202
|
+
|
|
203
|
+
Collection.new([!filter(receiver, expression, context).empty?])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def negate(receiver, node)
|
|
207
|
+
return Collection.empty if receiver.empty?
|
|
208
|
+
|
|
209
|
+
value = require_singleton(receiver, node.receiver ? node.receiver.span : node.span)
|
|
210
|
+
unless [true, false].include?(value)
|
|
211
|
+
raise TypeError.new('expected a Boolean value', code: :expected_boolean, span: node.span)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
Collection.new([!value])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def integer_argument(node, context)
|
|
218
|
+
argument = node.arguments.first
|
|
219
|
+
value = evaluate(argument, context)
|
|
220
|
+
count = require_singleton(value, argument.span)
|
|
221
|
+
unless count.is_a?(::Integer)
|
|
222
|
+
raise TypeError.new('expected an integer argument', code: :expected_integer, span: argument.span)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
count
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def take(receiver, count)
|
|
229
|
+
return Collection.empty if receiver.empty? || count <= 0
|
|
230
|
+
return Collection.new(receiver.items) if count >= receiver.count
|
|
231
|
+
|
|
232
|
+
Collection.new(receiver.items.first(count))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def skip(receiver, count)
|
|
236
|
+
return Collection.new(receiver.items) if count <= 0
|
|
237
|
+
return Collection.empty if count >= receiver.count
|
|
238
|
+
|
|
239
|
+
Collection.new(receiver.items[count..])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Aggregates (FHIRPath 2.0.0 count(); FHIRPath 3.0.0 STU3 sum/avg/max/min).
|
|
243
|
+
# Each returns a fresh immutable collection and never mutates the input.
|
|
244
|
+
# sum()/avg() promote Integer and Decimal items through BigDecimal when any
|
|
245
|
+
# decimal participates (matching the evaluator's numeric promotion in
|
|
246
|
+
# arithmetic and equality); max()/min() reuse the comparison operator
|
|
247
|
+
# semantics so numerics compare across Integer/Decimal and strings order
|
|
248
|
+
# lexicographically, with incompatible item types raising a TypeError.
|
|
249
|
+
def sum(receiver, node)
|
|
250
|
+
return Collection.empty if receiver.empty?
|
|
251
|
+
unless receiver.items.all? { |item| numeric?(item) }
|
|
252
|
+
raise TypeError.new('sum requires numeric items', code: :expected_number, span: node.span)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
result = if receiver.items.all?(::Integer)
|
|
256
|
+
receiver.items.reduce(:+)
|
|
257
|
+
else
|
|
258
|
+
receiver.items.reduce { |total, item| decimal(total) + decimal(item) }
|
|
259
|
+
end
|
|
260
|
+
Collection.new([result])
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def average(receiver, node)
|
|
264
|
+
return Collection.empty if receiver.empty?
|
|
265
|
+
unless receiver.items.all? { |item| numeric?(item) }
|
|
266
|
+
raise TypeError.new('avg requires numeric items', code: :expected_number, span: node.span)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
total = receiver.items.reduce { |sum, item| decimal(sum) + decimal(item) }
|
|
270
|
+
Collection.new([decimal(total) / receiver.count])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def extremum(receiver, mode, node)
|
|
274
|
+
return Collection.empty if receiver.empty?
|
|
275
|
+
|
|
276
|
+
result = receiver.items.first
|
|
277
|
+
receiver.items.drop(1).each do |item|
|
|
278
|
+
comparison = compare_values(result, item, node.span)
|
|
279
|
+
result = item if mode == 'max' ? comparison.negative? : comparison.positive?
|
|
280
|
+
end
|
|
281
|
+
Collection.new([result])
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def all(receiver, predicate, context)
|
|
285
|
+
result = receiver.items.each_with_index.all? do |item, position|
|
|
286
|
+
predicate_result = evaluate(
|
|
287
|
+
predicate,
|
|
288
|
+
context.derive(focus: Collection.new([item]), index: position, total: receiver.count)
|
|
289
|
+
)
|
|
290
|
+
boolean_value(predicate_result, predicate.span) == true
|
|
291
|
+
end
|
|
292
|
+
Collection.new([result])
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def boolean_aggregate(receiver, name, node)
|
|
296
|
+
values = receiver.items.map do |value|
|
|
297
|
+
unless [true, false].include?(value)
|
|
298
|
+
raise TypeError.new('expected a Boolean value', code: :expected_boolean, span: node.span)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
value
|
|
302
|
+
end
|
|
303
|
+
result = case name
|
|
304
|
+
when 'allTrue' then values.all?
|
|
305
|
+
when 'anyTrue' then values.any?
|
|
306
|
+
when 'allFalse' then values.all?(&:!)
|
|
307
|
+
when 'anyFalse' then values.any?(&:!)
|
|
308
|
+
end
|
|
309
|
+
Collection.new([result])
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def invoke_registered(node, receiver, context, spec)
|
|
313
|
+
unless spec.implementation
|
|
314
|
+
raise UnsupportedFeatureError.new(
|
|
315
|
+
"function #{node.name} has no implementation", code: :unsupported_function,
|
|
316
|
+
span: node.span
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
arguments = node.arguments.each_with_index.map do |argument, position|
|
|
321
|
+
parameter = spec.parameters[position]
|
|
322
|
+
delayed = parameter.nil? ? spec.delayed : parameter == :expression
|
|
323
|
+
delayed ? argument : evaluate(argument, context)
|
|
324
|
+
end
|
|
325
|
+
Collection.from(spec.implementation.call(receiver, arguments, context))
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def filter(receiver, predicate, context)
|
|
329
|
+
matching = receiver.items.each_with_index.with_object([]) do |(item, position), result|
|
|
330
|
+
predicate_result = evaluate(
|
|
331
|
+
predicate,
|
|
332
|
+
context.derive(focus: Collection.new([item]), index: position, total: receiver.count)
|
|
333
|
+
)
|
|
334
|
+
result << item if truthy?(predicate_result, predicate.span)
|
|
335
|
+
end
|
|
336
|
+
Collection.new(matching)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def select(receiver, expression, context)
|
|
340
|
+
Collection.new(receiver.items.each_with_index.flat_map do |item, position|
|
|
341
|
+
evaluate(
|
|
342
|
+
expression,
|
|
343
|
+
context.derive(focus: Collection.new([item]), index: position, total: receiver.count)
|
|
344
|
+
).items
|
|
345
|
+
end)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def valid_arity?(arity, actual)
|
|
349
|
+
arity.is_a?(Range) ? arity.cover?(actual) : arity.nil? || arity == actual
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def unary(node, context)
|
|
353
|
+
value = evaluate(node.operand, context)
|
|
354
|
+
return Collection.empty if value.empty?
|
|
355
|
+
|
|
356
|
+
number = require_singleton(value, node.operand.span)
|
|
357
|
+
unless numeric?(number)
|
|
358
|
+
raise TypeError.new('unary arithmetic requires a number', code: :expected_number,
|
|
359
|
+
span: node.span)
|
|
360
|
+
end
|
|
361
|
+
result = node.operator == :minus ? -number : number
|
|
362
|
+
Collection.new([result])
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def binary(node, context)
|
|
366
|
+
case node.operator
|
|
367
|
+
when :equals, :not_equals, :equivalent, :not_equivalent
|
|
368
|
+
equality(node, context)
|
|
369
|
+
when :and, :or, :xor, :implies
|
|
370
|
+
logic(node, context)
|
|
371
|
+
when :plus, :minus, :multiply, :divide, :div, :mod
|
|
372
|
+
arithmetic(node, context)
|
|
373
|
+
when :concatenate
|
|
374
|
+
concatenate(node, context)
|
|
375
|
+
when :union
|
|
376
|
+
union(node, context)
|
|
377
|
+
when :in, :contains
|
|
378
|
+
membership(node, context)
|
|
379
|
+
when :is, :as
|
|
380
|
+
type_operator(node, context)
|
|
381
|
+
when :less_than, :less_or_equal, :greater_than, :greater_or_equal
|
|
382
|
+
comparison(node, context)
|
|
383
|
+
else
|
|
384
|
+
raise UnsupportedFeatureError.new("operator #{node.operator} is not supported",
|
|
385
|
+
code: :unsupported_operator, span: node.span)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def equality(node, context)
|
|
390
|
+
left = evaluate(node.left, context)
|
|
391
|
+
right = evaluate(node.right, context)
|
|
392
|
+
equivalent = %i[equivalent not_equivalent].include?(node.operator)
|
|
393
|
+
|
|
394
|
+
return Collection.new([node.operator == :equivalent]) if equivalent && left.empty? && right.empty?
|
|
395
|
+
return Collection.new([node.operator == :not_equivalent]) if equivalent && (left.empty? || right.empty?)
|
|
396
|
+
return Collection.empty if left.empty? || right.empty?
|
|
397
|
+
|
|
398
|
+
equal = if equivalent
|
|
399
|
+
collection_equivalent?(left, right)
|
|
400
|
+
else
|
|
401
|
+
collection_equal?(left, right)
|
|
402
|
+
end
|
|
403
|
+
equal = !equal if %i[not_equals not_equivalent].include?(node.operator)
|
|
404
|
+
Collection.new([equal])
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def collection_equal?(left, right)
|
|
408
|
+
return false unless left.count == right.count
|
|
409
|
+
|
|
410
|
+
left.items.zip(right.items).all? { |a, b| equal?(a, b) }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def collection_equivalent?(left, right)
|
|
414
|
+
return false unless left.count == right.count
|
|
415
|
+
|
|
416
|
+
remaining = right.items.dup
|
|
417
|
+
left.items.all? do |left_value|
|
|
418
|
+
match = remaining.index { |right_value| equivalent?(left_value, right_value) }
|
|
419
|
+
next false unless match
|
|
420
|
+
|
|
421
|
+
remaining.delete_at(match)
|
|
422
|
+
true
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def logic(node, context)
|
|
427
|
+
left = boolean_value(evaluate(node.left, context), node.left.span)
|
|
428
|
+
right = boolean_value(evaluate(node.right, context), node.right.span)
|
|
429
|
+
Collection.from(logical_result(node.operator, left, right))
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def logical_result(operator, left, right)
|
|
433
|
+
case operator
|
|
434
|
+
when :and
|
|
435
|
+
return false if [left, right].include?(false)
|
|
436
|
+
return nil if left.nil? || right.nil?
|
|
437
|
+
|
|
438
|
+
true
|
|
439
|
+
when :or
|
|
440
|
+
return true if [left, right].include?(true)
|
|
441
|
+
return nil if left.nil? || right.nil?
|
|
442
|
+
|
|
443
|
+
false
|
|
444
|
+
when :xor
|
|
445
|
+
return nil if left.nil? || right.nil?
|
|
446
|
+
|
|
447
|
+
left != right
|
|
448
|
+
when :implies
|
|
449
|
+
return true if left == false || right == true
|
|
450
|
+
return nil if left.nil? || right.nil?
|
|
451
|
+
|
|
452
|
+
false
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def arithmetic(node, context)
|
|
457
|
+
left = evaluate(node.left, context)
|
|
458
|
+
right = evaluate(node.right, context)
|
|
459
|
+
return Collection.empty if left.empty? || right.empty?
|
|
460
|
+
|
|
461
|
+
left_value = require_singleton(left, node.left.span)
|
|
462
|
+
right_value = require_singleton(right, node.right.span)
|
|
463
|
+
if left_value.is_a?(::String) && right_value.is_a?(::String) && node.operator == :plus
|
|
464
|
+
return Collection.new([left_value + right_value])
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
unless numeric?(left_value) && numeric?(right_value)
|
|
468
|
+
raise TypeError.new('arithmetic requires numeric values', code: :expected_number,
|
|
469
|
+
span: node.span)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Only `/`, `div`, `mod` divide. A zero divisor yields an EMPTY collection
|
|
473
|
+
# (FHIRPath 2.0.0, arithmetic operators); it is never an error. `+`, `-`,
|
|
474
|
+
# `*` operate on zero normally, so there is no blanket zero guard here.
|
|
475
|
+
result = case node.operator
|
|
476
|
+
when :plus then left_value + right_value
|
|
477
|
+
when :minus then left_value - right_value
|
|
478
|
+
when :multiply then left_value * right_value
|
|
479
|
+
when :divide
|
|
480
|
+
return Collection.empty if zero?(right_value)
|
|
481
|
+
|
|
482
|
+
decimal(left_value) / decimal(right_value)
|
|
483
|
+
when :div
|
|
484
|
+
return Collection.empty if zero?(right_value)
|
|
485
|
+
|
|
486
|
+
(decimal(left_value) / decimal(right_value)).truncate
|
|
487
|
+
when :mod
|
|
488
|
+
return Collection.empty if zero?(right_value)
|
|
489
|
+
|
|
490
|
+
remainder(left_value, right_value)
|
|
491
|
+
end
|
|
492
|
+
Collection.new([result])
|
|
493
|
+
rescue ::ZeroDivisionError
|
|
494
|
+
Collection.empty
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def comparison(node, context)
|
|
498
|
+
left = evaluate(node.left, context)
|
|
499
|
+
right = evaluate(node.right, context)
|
|
500
|
+
return Collection.empty if left.empty? || right.empty?
|
|
501
|
+
|
|
502
|
+
left_value = require_singleton(left, node.left.span)
|
|
503
|
+
right_value = require_singleton(right, node.right.span)
|
|
504
|
+
comparison = compare_values(left_value, right_value, node.span)
|
|
505
|
+
result = case node.operator
|
|
506
|
+
when :less_than then comparison.negative?
|
|
507
|
+
when :less_or_equal then comparison <= 0
|
|
508
|
+
when :greater_than then comparison.positive?
|
|
509
|
+
when :greater_or_equal then comparison >= 0
|
|
510
|
+
end
|
|
511
|
+
Collection.new([result])
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def concatenate(node, context)
|
|
515
|
+
left = evaluate(node.left, context)
|
|
516
|
+
right = evaluate(node.right, context)
|
|
517
|
+
left_value = left.empty? ? '' : require_singleton(left, node.left.span)
|
|
518
|
+
right_value = right.empty? ? '' : require_singleton(right, node.right.span)
|
|
519
|
+
unless left_value.is_a?(::String) && right_value.is_a?(::String)
|
|
520
|
+
raise TypeError.new('concatenation requires strings', code: :expected_string, span: node.span)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
Collection.new([left_value + right_value])
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def union(node, context)
|
|
527
|
+
left = evaluate(node.left, context)
|
|
528
|
+
right = evaluate(node.right, context)
|
|
529
|
+
# Union eliminates duplicate values from BOTH operands (FHIRPath 2.0.0,
|
|
530
|
+
# 5.4.1 union), using the `=` (Equals) predicate rather than Ruby value
|
|
531
|
+
# identity so that e.g. Integer 1 and Decimal 1.0 collapse to one value.
|
|
532
|
+
# First-seen order is preserved; operands are not mutated.
|
|
533
|
+
result = []
|
|
534
|
+
(left.items + right.items).each do |candidate|
|
|
535
|
+
result << candidate unless result.any? { |existing| equal?(existing, candidate) }
|
|
536
|
+
end
|
|
537
|
+
Collection.new(result)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def membership(node, context)
|
|
541
|
+
left = evaluate(node.left, context)
|
|
542
|
+
right = evaluate(node.right, context)
|
|
543
|
+
if node.operator == :in
|
|
544
|
+
return Collection.empty if left.empty?
|
|
545
|
+
|
|
546
|
+
value = require_singleton(left, node.left.span)
|
|
547
|
+
return Collection.new([false]) if right.empty?
|
|
548
|
+
|
|
549
|
+
return Collection.new([right.items.any? { |candidate| equal?(value, candidate) }])
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# `contains`: the right operand is the value being searched for and must be
|
|
553
|
+
# a singleton (FHIRPath 2.0.0 membership operators). This still holds when
|
|
554
|
+
# the left (searched) collection is empty, so the cardinality check is done
|
|
555
|
+
# before the empty-left short-circuit. An empty right operand yields an
|
|
556
|
+
# empty result.
|
|
557
|
+
return Collection.empty if right.empty?
|
|
558
|
+
|
|
559
|
+
value = require_singleton(right, node.right.span)
|
|
560
|
+
return Collection.new([false]) if left.empty?
|
|
561
|
+
|
|
562
|
+
Collection.new([left.items.any? { |candidate| equal?(candidate, value) }])
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def type_operator(node, context)
|
|
566
|
+
left = evaluate(node.left, context)
|
|
567
|
+
type_name = type_name_from(node.right, node.span)
|
|
568
|
+
return Collection.empty if left.empty?
|
|
569
|
+
|
|
570
|
+
value = require_singleton(left, node.left.span)
|
|
571
|
+
matches = logical_type?(value, type_name) || model_logical_type?(left, type_name)
|
|
572
|
+
return Collection.new([matches]) if node.operator == :is
|
|
573
|
+
|
|
574
|
+
matches ? Collection.new([value]) : Collection.empty
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def type_name_from(node, span)
|
|
578
|
+
return node.name.downcase if node.is_a?(AST::Identifier)
|
|
579
|
+
|
|
580
|
+
raise TypeError.new('type operator requires a type name', code: :expected_type, span: span)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# FHIR model type recorded when the item was produced by model navigation
|
|
584
|
+
# (e.g. `Quantity` for a resolved `Observation.valueQuantity` choice). The
|
|
585
|
+
# FHIR type name is compared case-insensitively because `type_name_from`
|
|
586
|
+
# downcases the identifier on the right of `is`/`as`.
|
|
587
|
+
def model_logical_type?(collection, type_name)
|
|
588
|
+
recorded = collection.types&.first
|
|
589
|
+
!recorded.nil? && recorded.downcase == type_name
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def logical_type?(value, type_name)
|
|
593
|
+
case type_name
|
|
594
|
+
when 'any' then true
|
|
595
|
+
when 'boolean' then [true, false].include?(value)
|
|
596
|
+
when 'integer' then value.is_a?(::Integer)
|
|
597
|
+
when 'decimal' then value.is_a?(BigDecimal) || (value.is_a?(::Float) && value.finite?)
|
|
598
|
+
when 'number' then numeric?(value)
|
|
599
|
+
when 'string' then value.is_a?(::String)
|
|
600
|
+
else false
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def compare_values(left, right, span)
|
|
605
|
+
if numeric?(left) && numeric?(right)
|
|
606
|
+
decimal(left) <=> decimal(right)
|
|
607
|
+
elsif left.is_a?(::String) && right.is_a?(::String)
|
|
608
|
+
left <=> right
|
|
609
|
+
else
|
|
610
|
+
raise TypeError.new('comparison requires compatible values', code: :incompatible_comparison,
|
|
611
|
+
span: span)
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def equal?(left, right)
|
|
616
|
+
return decimal(left) == decimal(right) if numeric?(left) && numeric?(right)
|
|
617
|
+
return false unless left.class == right.class
|
|
618
|
+
|
|
619
|
+
left == right
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def equivalent?(left, right)
|
|
623
|
+
if left.is_a?(::String) && right.is_a?(::String)
|
|
624
|
+
normalize_string(left) == normalize_string(right)
|
|
625
|
+
elsif numeric?(left) && numeric?(right)
|
|
626
|
+
decimal_equivalent?(left, right)
|
|
627
|
+
else
|
|
628
|
+
equal?(left, right)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def normalize_string(value)
|
|
633
|
+
value.strip.downcase.gsub(/\s+/, ' ')
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def decimal_equivalent?(left, right)
|
|
637
|
+
precision = [decimal_places(left), decimal_places(right)].min
|
|
638
|
+
decimal(left).round(precision) == decimal(right).round(precision)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def decimal_places(value)
|
|
642
|
+
text = decimal(value).to_s('F')
|
|
643
|
+
fraction = text.split('.', 2)[1]
|
|
644
|
+
fraction ? fraction.sub(/0+\z/, '').length : 0
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def numeric?(value)
|
|
648
|
+
value.is_a?(::Integer) ||
|
|
649
|
+
value.is_a?(BigDecimal) ||
|
|
650
|
+
(value.is_a?(::Float) && value.finite?)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def decimal(value)
|
|
654
|
+
# Finite Float (e.g. from JSON.parse) is a Decimal in FHIRPath; convert it
|
|
655
|
+
# losslessly without mutating the source value. NaN/Infinity never reach
|
|
656
|
+
# here because numeric? rejects them.
|
|
657
|
+
value.is_a?(BigDecimal) ? value : BigDecimal(value.to_s)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def remainder(left, right)
|
|
661
|
+
quotient = (decimal(left) / decimal(right)).truncate
|
|
662
|
+
result = left - (quotient * right)
|
|
663
|
+
left.is_a?(BigDecimal) || right.is_a?(BigDecimal) ? decimal(result) : result
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def zero?(value)
|
|
667
|
+
value.zero?
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def require_singleton(collection, span)
|
|
671
|
+
return collection.first_item if collection.singleton?
|
|
672
|
+
|
|
673
|
+
raise SingletonError.new('a singleton value was required', code: :singleton_required,
|
|
674
|
+
span: span)
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def truthy?(collection, span)
|
|
678
|
+
boolean_value(collection, span) == true
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def boolean_value(collection, span)
|
|
682
|
+
return nil if collection.empty?
|
|
683
|
+
|
|
684
|
+
value = require_singleton(collection, span)
|
|
685
|
+
return value if [true, false].include?(value)
|
|
686
|
+
|
|
687
|
+
raise TypeError.new('expected a Boolean value', code: :expected_boolean, span: span)
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
end
|