@atomic-ehr/fhirpath
Version:
A TypeScript implementation of FHIRPath
773 lines (666 loc) • 31.7 kB
text/typescript
import type {
ASTNode,
LiteralNode,
IdentifierNode,
BinaryNode,
UnaryNode,
FunctionNode,
VariableNode,
CollectionNode,
IndexNode,
TypeOrIdentifierNode,
MembershipTestNode,
TypeCastNode,
QuantityNode
} from './types';
import { NodeType } from './types';
import { Registry } from './registry';
import * as operations from './operations';
import type { EvaluationResult, FunctionEvaluator, NodeEvaluator, OperationEvaluator, RuntimeContext } from './types';
import { createQuantity } from './quantity-value';
import { box, unbox, ensureBoxed, type FHIRPathValue } from './boxing';
import { Errors } from './errors';
/**
* Runtime context manager that provides efficient prototype-based context operations
* for both interpreter and compiler.
*/
export class RuntimeContextManager {
/**
* Create a new runtime context
*/
static create(input: any[], initialVariables?: Record<string, any>): RuntimeContext {
const context = Object.create(null) as RuntimeContext;
context.input = input;
context.focus = input;
// Create variables object with null prototype to avoid pollution
context.variables = Object.create(null);
// Set root context variables with % prefix
context.variables['%context'] = input;
context.variables['%resource'] = input;
context.variables['%rootResource'] = input;
// Add any initial variables (with % prefix for user-defined)
if (initialVariables) {
for (const [key, value] of Object.entries(initialVariables)) {
// Add % prefix if not already present and not a special variable
const varKey = key.startsWith('$') || key.startsWith('%') ? key : `%${key}`;
context.variables[varKey] = value;
}
}
return context;
}
/**
* Create a child context using prototype inheritance
* O(1) operation - no copying needed
*/
static copy(context: RuntimeContext): RuntimeContext {
// Create child context with parent as prototype
const newContext = Object.create(context) as RuntimeContext;
// Create child variables that inherit from parent's variables
newContext.variables = Object.create(context.variables);
// input and focus are inherited through prototype chain
// Only set them if they need to change
return newContext;
}
/**
* Create a new context with updated input/focus
*/
static withInput(context: RuntimeContext, input: any[], focus?: any[]): RuntimeContext {
const newContext = this.copy(context);
newContext.input = input;
newContext.focus = focus ?? input;
return newContext;
}
/**
* Set iterator context ($this, $index)
*/
static withIterator(
context: RuntimeContext,
item: any,
index: number
): RuntimeContext {
let newContext = this.setVariable(context, '$this', [item], true);
newContext = this.setVariable(newContext, '$index', index, true);
return newContext;
}
/**
* Set a variable in the context (handles both special $ and user % variables)
*/
static setVariable(context: RuntimeContext, name: string, value: any, allowRedefinition: boolean = false): RuntimeContext {
// Ensure value is array for consistency (except for special variables like $index)
const arrayValue = (name === '$index' || name === '$total') ? value :
Array.isArray(value) ? value : [value];
// Determine variable key based on prefix
let varKey = name;
if (!name.startsWith('$') && !name.startsWith('%')) {
// No prefix - assume user-defined variable, add % prefix
varKey = `%${name}`;
}
// Check for system variables (with or without % prefix)
const systemVariables = ['context', 'resource', 'rootResource', 'ucum', 'sct', 'loinc'];
const baseVarName = varKey.startsWith('%') ? varKey.substring(1) : varKey;
if (systemVariables.includes(baseVarName)) {
// Silently return original context for system variable redefinition
return context;
}
// Check if variable already exists (unless redefinition is allowed)
if (!allowRedefinition && context.variables && Object.prototype.hasOwnProperty.call(context.variables, varKey)) {
// Silently return original context for variable redefinition
return context;
}
// Create new context and set variable
const newContext = this.copy(context);
newContext.variables[varKey] = arrayValue;
// Special handling for $this
if (varKey === '$this' && Array.isArray(arrayValue) && arrayValue.length === 1) {
newContext.input = arrayValue;
newContext.focus = arrayValue;
}
return newContext;
}
/**
* Get a variable from context
*/
static getVariable(context: RuntimeContext, name: string): any | undefined {
// Handle special cases
if (name === '$this' || name === '$index' || name === '$total') {
return context.variables[name];
}
// Handle environment variables (with or without % prefix)
if (name === 'context' || name === '%context') {
return context.variables['%context'];
}
if (name === 'resource' || name === '%resource') {
return context.variables['%resource'];
}
if (name === 'rootResource' || name === '%rootResource') {
return context.variables['%rootResource'];
}
// Handle user-defined variables (add % prefix if not present)
const varKey = name.startsWith('%') ? name : `%${name}`;
return context.variables[varKey];
}
}
export class Interpreter {
private registry: Registry;
private nodeEvaluators: Record<NodeType, NodeEvaluator>;
private operationEvaluators: Map<string, OperationEvaluator>;
private functionEvaluators: Map<string, FunctionEvaluator>;
private modelProvider?: import('./types').ModelProvider<any>;
constructor(registry?: Registry, modelProvider?: import('./types').ModelProvider<any>) {
this.registry = registry || new Registry();
this.modelProvider = modelProvider;
this.operationEvaluators = new Map();
this.functionEvaluators = new Map();
// Initialize node evaluators using object dispatch pattern
this.nodeEvaluators = {
[NodeType.Literal]: this.evaluateLiteral.bind(this),
[NodeType.Identifier]: this.evaluateIdentifier.bind(this),
[NodeType.TypeOrIdentifier]: this.evaluateTypeOrIdentifier.bind(this),
[NodeType.Binary]: this.evaluateBinary.bind(this),
[NodeType.Unary]: this.evaluateUnary.bind(this),
[NodeType.Function]: this.evaluateFunction.bind(this),
[NodeType.Variable]: this.evaluateVariable.bind(this),
[NodeType.Collection]: this.evaluateCollection.bind(this),
[NodeType.Index]: this.evaluateIndex.bind(this),
[NodeType.MembershipTest]: this.evaluateMembershipTest.bind(this),
[NodeType.TypeCast]: this.evaluateTypeCast.bind(this),
[NodeType.Quantity]: this.evaluateQuantity.bind(this),
[NodeType.EOF]: async () => ({ value: [], context: {} as RuntimeContext }),
[NodeType.TypeReference]: async () => ({ value: [], context: {} as RuntimeContext })
};
// Register operation evaluators
this.registerOperationEvaluators();
}
private registerOperationEvaluators(): void {
// Register evaluators from operations modules
for (const [name, operation] of Object.entries(operations)) {
if (typeof operation === 'object' && 'evaluate' in operation) {
if ('symbol' in operation) {
// It's an operator
// Skip unary operators here - they're handled differently
if (name === 'unaryMinusOperator' || name === 'unaryPlusOperator') {
continue;
}
this.operationEvaluators.set(operation.symbol, operation.evaluate);
} else if ('signatures' in operation && !('symbol' in operation)) {
// It's a function
this.functionEvaluators.set(operation.name, operation.evaluate);
}
}
}
}
// Main evaluate method
async evaluate(node: ASTNode, input: any[] = [], context?: RuntimeContext): Promise<EvaluationResult> {
// Initialize context if not provided
if (!context) {
context = this.createInitialContext(input);
}
// Ensure input is always an array
if (!Array.isArray(input)) {
input = input === null || input === undefined ? [] : [input];
}
// Box the initial input values
const boxedInput = input.map(value => ensureBoxed(value));
// Set current node in context
const contextWithNode = RuntimeContextManager.copy(context);
contextWithNode.currentNode = node;
// Dispatch to appropriate evaluator
const evaluator = this.nodeEvaluators[node.type];
if (!evaluator) {
throw Errors.unknownNodeType(node.type);
}
return await evaluator(node, boxedInput, contextWithNode);
}
private createInitialContext(input: any[]): RuntimeContext {
const context = RuntimeContextManager.create(input);
// Set $this to initial input
context.variables['$this'] = input;
// Add model provider if available
if (this.modelProvider) {
context.modelProvider = this.modelProvider;
}
return context;
}
// Literal node evaluator
private async evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const literal = node as LiteralNode;
// Box the literal value with appropriate type info
let typeInfo: import('./types').TypeInfo | undefined;
const value = literal.value;
if (typeof value === 'string') {
typeInfo = { type: 'String', singleton: true };
} else if (typeof value === 'number') {
typeInfo = Number.isInteger(value) ?
{ type: 'Integer', singleton: true } :
{ type: 'Decimal', singleton: true };
} else if (typeof value === 'boolean') {
typeInfo = { type: 'Boolean', singleton: true };
}
return {
value: [box(literal.value, typeInfo)],
context
};
}
// Identifier node evaluator
private async evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const identifier = node as IdentifierNode;
const name = identifier.name;
// Navigate property on each boxed item in input
const results: FHIRPathValue[] = [];
// Get the type info from the node (set by analyzer)
const nodeTypeInfo = node.typeInfo;
for (const boxedItem of input) {
const item = unbox(boxedItem);
// Special handling for primitive extension navigation
if (name === 'extension' && boxedItem.primitiveElement?.extension) {
// Navigation from a primitive value to its extensions
for (const ext of boxedItem.primitiveElement.extension) {
results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
}
continue;
}
if (item && typeof item === 'object') {
// First, check if this might be a FHIR choice type by looking for choice properties
// FHIR choice types use the pattern: base name + Type suffix (e.g., valueQuantity, valueCodeableConcept)
let foundChoiceValue = false;
// Check for FHIR choice type pattern
if (context.modelProvider) {
// Try common FHIR choice patterns
const possibleChoiceProperties = Object.keys(item).filter(key =>
key.startsWith(name) && key !== name && key.length > name.length
);
if (possibleChoiceProperties.length > 0) {
// This looks like a choice type - return all matching values
for (const choiceProp of possibleChoiceProperties) {
const value = item[choiceProp];
const primitiveElementName = `_${choiceProp}`;
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
// Try to determine the type from the property name suffix
let choiceType = 'Any';
const suffix = choiceProp.substring(name.length);
if (suffix) {
// Remove leading uppercase letter and make it the type
choiceType = suffix;
}
if (Array.isArray(value)) {
for (const v of value) {
// For FHIR resources, use their resourceType
if (v && typeof v === 'object' && 'resourceType' in v) {
const typeInfo = await context.modelProvider!.getType(v.resourceType);
results.push(box(v, typeInfo || { type: v.resourceType as any, singleton: true }, primitiveElement));
} else {
results.push(box(v, { type: choiceType as any, singleton: true }, primitiveElement));
}
}
} else if (value !== null && value !== undefined) {
// For FHIR resources, use their resourceType
if (value && typeof value === 'object' && 'resourceType' in value) {
const typeInfo = await context.modelProvider!.getType(value.resourceType);
results.push(box(value, typeInfo || { type: value.resourceType as any, singleton: true }, primitiveElement));
} else {
results.push(box(value, { type: choiceType as any, singleton: !Array.isArray(value) }, primitiveElement));
}
}
foundChoiceValue = true;
}
}
}
// Check if this is a choice type navigation from analyzer
if (!foundChoiceValue && nodeTypeInfo?.modelContext && typeof nodeTypeInfo.modelContext === 'object' &&
'isUnion' in nodeTypeInfo.modelContext &&
nodeTypeInfo.modelContext.isUnion && 'choices' in nodeTypeInfo.modelContext &&
Array.isArray(nodeTypeInfo.modelContext.choices)) {
// For choice types, look for any of the choice properties
for (const choice of nodeTypeInfo.modelContext.choices) {
const choiceName = choice.choiceName;
if (choiceName && choiceName in item) {
const value = item[choiceName];
const primitiveElementName = `_${choiceName}`;
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
// Box with the specific choice type
const choiceTypeInfo = {
type: choice.type,
singleton: !Array.isArray(value),
modelContext: choice
};
if (Array.isArray(value)) {
for (const v of value) {
results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
}
} else if (value !== null && value !== undefined) {
results.push(box(value, choiceTypeInfo, primitiveElement));
}
foundChoiceValue = true;
}
}
}
if (!foundChoiceValue && name in item) {
// Regular property navigation
const value = item[name];
const primitiveElementName = `_${name}`;
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
if (Array.isArray(value)) {
// Box each array element with type info
// For arrays, make the type singleton since each element is a single value
const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
for (const v of value) {
// Special handling for FHIR resources - use their resourceType
// Do this when the property could be polymorphic (type is 'Any' or 'Resource')
if (v && typeof v === 'object' && 'resourceType' in v && typeof v.resourceType === 'string' &&
(!elementTypeInfo || elementTypeInfo.type === 'Any' || (elementTypeInfo as any).type === 'Resource')) {
// Get full type info from model provider if available
let resourceTypeInfo;
if (context.modelProvider) {
resourceTypeInfo = await context.modelProvider.getType(v.resourceType);
if (resourceTypeInfo) {
// Make it singleton since it's a single element in the array
resourceTypeInfo = { ...resourceTypeInfo, singleton: true };
}
}
if (!resourceTypeInfo) {
// Fallback to basic type info
resourceTypeInfo = {
type: v.resourceType as import('./types').TypeName,
singleton: true
};
}
results.push(box(v, resourceTypeInfo, primitiveElement));
} else {
results.push(box(v, elementTypeInfo, primitiveElement));
}
}
} else if (value !== null && value !== undefined) {
// Special handling for FHIR resources - use their resourceType
// Do this when the property could be polymorphic (type is 'Any' or 'Resource')
if (value && typeof value === 'object' && 'resourceType' in value && typeof value.resourceType === 'string' &&
(!nodeTypeInfo || nodeTypeInfo.type === 'Any' || (nodeTypeInfo as any).type === 'Resource')) {
// Get full type info from model provider if available
let resourceTypeInfo;
if (context.modelProvider) {
resourceTypeInfo = await context.modelProvider.getType(value.resourceType);
if (resourceTypeInfo) {
// Preserve singleton status
resourceTypeInfo = { ...resourceTypeInfo, singleton: !Array.isArray(value) };
}
}
if (!resourceTypeInfo) {
// Fallback to basic type info
resourceTypeInfo = {
type: value.resourceType as import('./types').TypeName,
singleton: !Array.isArray(value)
};
}
results.push(box(value, resourceTypeInfo, primitiveElement));
} else {
// Box single value with primitive element if available
results.push(box(value, nodeTypeInfo, primitiveElement));
}
}
}
}
}
return {
value: results,
context
};
}
// TypeOrIdentifier node evaluator (handles Patient, Observation, etc.)
private async evaluateTypeOrIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const typeOrId = node as TypeOrIdentifierNode;
const name = typeOrId.name;
// First try as type filter
const filtered = input.filter(boxedItem => {
const item = unbox(boxedItem);
return item && typeof item === 'object' && item.resourceType === name;
});
if (filtered.length > 0) {
return { value: filtered, context };
}
// Otherwise treat as identifier
return await this.evaluateIdentifier(node, input, context);
}
// Binary operator evaluator
private async evaluateBinary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const binary = node as BinaryNode;
const operator = binary.operator;
// Special handling for dot operator (sequential pipeline)
if (operator === '.') {
// Evaluate left with current input/context
const leftResult = await this.evaluate(binary.left, input, context);
// Use left's output as right's input, and left's context flows to right
return await this.evaluate(binary.right, leftResult.value, leftResult.context);
}
// Special handling for union operator (each side gets fresh context from original)
if (operator === '|') {
// Each side of union should have its own variable scope
// Variables defined on left side should not be visible on right side
const leftResult = await this.evaluate(binary.left, input, context);
const rightResult = await this.evaluate(binary.right, input, context); // Use original context, not leftResult.context
// Merge the results
const unionEvaluator = this.operationEvaluators.get('union');
if (unionEvaluator) {
return await unionEvaluator(input, context, leftResult.value, rightResult.value);
}
// Fallback if union evaluator not found
return {
value: [...leftResult.value, ...rightResult.value],
context // Original context preserved
};
}
// Get operation evaluator
const evaluator = this.operationEvaluators.get(operator);
if (evaluator) {
// Most operators evaluate arguments in parallel with same input/context
const leftResult = await this.evaluate(binary.left, input, context);
const rightResult = await this.evaluate(binary.right, input, context);
return await evaluator(input, context, leftResult.value, rightResult.value);
}
// If no evaluator found, throw error
throw Errors.noEvaluatorFound('binary operator', operator);
}
// Unary operator evaluator
private async evaluateUnary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const unary = node as UnaryNode;
const operator = unary.operator;
const operandResult = await this.evaluate(unary.operand, input, context);
// Check for unary operation evaluators
let evaluator: OperationEvaluator | undefined;
if (operator === '-' && operations.unaryMinusOperator?.evaluate) {
evaluator = operations.unaryMinusOperator.evaluate;
} else if (operator === '+' && operations.unaryPlusOperator?.evaluate) {
evaluator = operations.unaryPlusOperator.evaluate;
}
if (evaluator) {
return await evaluator(input, context, operandResult.value);
}
// If no evaluator found, throw error
throw Errors.noEvaluatorFound('unary operator', operator);
}
// Variable evaluator
private async evaluateVariable(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const variable = node as VariableNode;
const name = variable.name;
const value = RuntimeContextManager.getVariable(context, name);
if (value !== undefined) {
// Ensure value is always an array
const arrayValue = Array.isArray(value) ? value : [value];
// Box each value in the array
const boxedValues = arrayValue.map(v => ensureBoxed(v));
return { value: boxedValues, context };
}
// According to FHIRPath spec: attempting to access an undefined environment variable will result in an error
throw Errors.variableNotDefined(name);
}
// Collection evaluator
private async evaluateCollection(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const collection = node as CollectionNode;
const results: FHIRPathValue[] = [];
for (const element of collection.elements) {
const result = await this.evaluate(element, input, context);
results.push(...result.value);
}
return { value: results, context };
}
// Function evaluator
private async evaluateFunction(node: ASTNode, input: any[], context: RuntimeContext): Promise<EvaluationResult> {
const func = node as FunctionNode;
const funcName = (func.name as IdentifierNode).name;
// Check if function is registered with an evaluator
const functionEvaluator = this.functionEvaluators.get(funcName);
if (functionEvaluator) {
return await functionEvaluator(input, context, func.arguments, this.evaluate.bind(this));
}
// No function found in registry
throw Errors.unknownFunction(funcName);
}
// Index evaluator
private async evaluateIndex(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const indexNode = node as IndexNode;
const exprResult = await this.evaluate(indexNode.expression, input, context);
const indexResult = await this.evaluate(indexNode.index, input, context);
if (indexResult.value.length === 0 || exprResult.value.length === 0) {
return { value: [], context };
}
const boxedIndex = indexResult.value[0];
if (boxedIndex) {
const index = unbox(boxedIndex);
if (typeof index === 'number' && index >= 0 && index < exprResult.value.length) {
const result = exprResult.value[index];
return { value: result ? [result] : [], context };
}
}
return { value: [], context };
}
// Type membership test (is operator)
private async evaluateMembershipTest(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const test = node as MembershipTestNode;
const exprResult = await this.evaluate(test.expression, input, context);
// If expression evaluates to empty, return empty
if (exprResult.value.length === 0) {
return { value: [], context };
}
// If we have type information from analyzer (with ModelProvider), use it
if (context.currentNode?.typeInfo?.modelContext) {
const modelContext = context.currentNode.typeInfo.modelContext as any;
// For union types, check if the target type is valid
if (modelContext.isUnion && modelContext.choices) {
const hasValidChoice = modelContext.choices.some((c: any) =>
c.type === test.targetType || c.elementType === test.targetType
);
if (!hasValidChoice) {
// Type system knows this will always be false
return {
value: exprResult.value.map(() => box(false, { type: 'Boolean', singleton: true })),
context
};
}
}
}
// Type checking with subtype support via ModelProvider
const results = await Promise.all(exprResult.value.map(async boxedItem => {
const item = unbox(boxedItem);
// If we have a ModelProvider and typeInfo, use it for accurate subtype checking
if (context.modelProvider && boxedItem.typeInfo) {
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, test.targetType as import('./types').TypeName);
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
}
// For FHIR resources without typeInfo, try to get it from modelProvider
if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
const typeInfo = await context.modelProvider.getType(item.resourceType);
if (typeInfo) {
const matchingType = context.modelProvider.ofType(typeInfo, test.targetType as import('./types').TypeName);
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
}
// Fall back to exact match
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
}
// Check for FHIR resource types (no ModelProvider available)
if (item && typeof item === 'object' && 'resourceType' in item) {
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
}
// Check primitive types
const isMatch = (() => {
switch (test.targetType) {
case 'String': return typeof item === 'string';
case 'Boolean': return typeof item === 'boolean';
case 'Integer': return Number.isInteger(item);
case 'Decimal': return typeof item === 'number';
case 'Date':
case 'DateTime':
case 'Time':
// Simple check for date-like strings
return typeof item === 'string' && !isNaN(Date.parse(item));
default: return false;
}
})();
return box(isMatch, { type: 'Boolean', singleton: true });
}));
return { value: results, context };
}
// Type cast (as operator)
private async evaluateTypeCast(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const cast = node as TypeCastNode;
const exprResult = await this.evaluate(cast.expression, input, context);
// If we have type information from analyzer (with ModelProvider), use it
if (context.currentNode?.typeInfo?.modelContext) {
const modelContext = context.currentNode.typeInfo.modelContext as any;
// For union types, check if the cast is valid
if (modelContext.isUnion && modelContext.choices) {
const validChoice = modelContext.choices.find((c: any) =>
c.type === cast.targetType || c.elementType === cast.targetType
);
if (!validChoice) {
// Invalid cast - return empty
return { value: [], context };
}
}
}
// Filter values that match the target type with subtype support
const filtered = await Promise.all(exprResult.value.map(async boxedItem => {
const item = unbox(boxedItem);
// If we have a ModelProvider and typeInfo, use it for accurate subtype checking
if (context.modelProvider && boxedItem.typeInfo) {
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, cast.targetType as import('./types').TypeName);
return matchingType !== undefined;
}
// For FHIR resources without typeInfo, try to get it from modelProvider
if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
const typeInfo = await context.modelProvider.getType(item.resourceType);
if (typeInfo) {
const matchingType = context.modelProvider.ofType(typeInfo, cast.targetType as import('./types').TypeName);
return matchingType !== undefined;
}
// Fall back to exact match
return item.resourceType === cast.targetType;
}
// Check for FHIR resource types (no ModelProvider available)
if (item && typeof item === 'object' && 'resourceType' in item) {
return item.resourceType === cast.targetType;
}
// Check primitive types
switch (cast.targetType) {
case 'String': return typeof item === 'string';
case 'Boolean': return typeof item === 'boolean';
case 'Integer': return Number.isInteger(item);
case 'Decimal': return typeof item === 'number';
case 'Date':
case 'DateTime':
case 'Time':
// Simple check for date-like strings
return typeof item === 'string' && !isNaN(Date.parse(item));
default: return false;
}
}));
// Filter out the false results (filter returns boolean for each item)
const actualFiltered = exprResult.value.filter((_, index) => filtered[index]);
return { value: actualFiltered, context };
}
private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
const quantity = node as QuantityNode;
const quantityValue = createQuantity(quantity.value, quantity.unit, quantity.isCalendarUnit);
return {
value: [box(quantityValue, { type: 'Quantity', singleton: true })],
context
};
}
}