@atomic-ehr/fhirpath 0.0.3 → 0.0.4
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.
- package/dist/index.browser.d.ts +22 -0
- package/dist/index.browser.js +15758 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.node.d.ts +24 -0
- package/dist/{index.js → index.node.js} +5450 -3809
- package/dist/index.node.js.map +1 -0
- package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
- package/package.json +10 -5
- package/src/analyzer.ts +46 -9
- package/src/complex-types/quantity-value.ts +131 -9
- package/src/complex-types/temporal.ts +45 -6
- package/src/errors.ts +4 -0
- package/src/index.browser.ts +4 -0
- package/src/{index.ts → index.common.ts} +18 -14
- package/src/index.node.ts +4 -0
- package/src/interpreter/navigator.ts +12 -0
- package/src/interpreter/runtime-context.ts +60 -25
- package/src/interpreter.ts +118 -33
- package/src/lexer.ts +4 -1
- package/src/model-provider.browser.ts +35 -0
- package/src/{model-provider.ts → model-provider.common.ts} +29 -26
- package/src/model-provider.node.ts +41 -0
- package/src/operations/allTrue-function.ts +6 -10
- package/src/operations/and-operator.ts +2 -2
- package/src/operations/as-function.ts +41 -0
- package/src/operations/combine-operator.ts +17 -4
- package/src/operations/comparison.ts +73 -21
- package/src/operations/convertsToQuantity-function.ts +56 -7
- package/src/operations/decode-function.ts +114 -0
- package/src/operations/divide-operator.ts +3 -3
- package/src/operations/encode-function.ts +110 -0
- package/src/operations/escape-function.ts +114 -0
- package/src/operations/exp-function.ts +65 -0
- package/src/operations/extension-function.ts +88 -0
- package/src/operations/greater-operator.ts +5 -24
- package/src/operations/greater-or-equal-operator.ts +5 -24
- package/src/operations/hasValue-function.ts +84 -0
- package/src/operations/iif-function.ts +7 -1
- package/src/operations/implies-operator.ts +1 -0
- package/src/operations/index.ts +11 -0
- package/src/operations/is-function.ts +11 -0
- package/src/operations/is-operator.ts +187 -5
- package/src/operations/less-operator.ts +6 -24
- package/src/operations/less-or-equal-operator.ts +5 -24
- package/src/operations/less-than.ts +7 -12
- package/src/operations/ln-function.ts +62 -0
- package/src/operations/log-function.ts +113 -0
- package/src/operations/lowBoundary-function.ts +14 -0
- package/src/operations/minus-operator.ts +8 -1
- package/src/operations/mod-operator.ts +7 -1
- package/src/operations/not-function.ts +9 -2
- package/src/operations/ofType-function.ts +35 -0
- package/src/operations/plus-operator.ts +46 -3
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/replace-function.ts +19 -19
- package/src/operations/replaceMatches-function.ts +5 -0
- package/src/operations/sort-function.ts +209 -0
- package/src/operations/take-function.ts +1 -1
- package/src/operations/toQuantity-function.ts +0 -1
- package/src/operations/toString-function.ts +76 -12
- package/src/operations/trace-function.ts +20 -3
- package/src/operations/unescape-function.ts +119 -0
- package/src/operations/where-function.ts +3 -1
- package/src/parser.ts +14 -2
- package/src/types.ts +7 -5
- package/src/utils/decimal.ts +76 -0
- package/dist/index.js.map +0 -1
|
@@ -190,13 +190,13 @@ declare enum PRECEDENCE {
|
|
|
190
190
|
XOR = 30,
|
|
191
191
|
AND = 40,
|
|
192
192
|
IN_CONTAINS = 50,
|
|
193
|
+
AS_IS = 55,// as, is (moved to lower precedence)
|
|
193
194
|
EQUALITY = 60,// =, !=, ~, !~
|
|
195
|
+
PIPE = 65,// | (moved to lower precedence)
|
|
194
196
|
COMPARISON = 70,// <, >, <=, >=
|
|
195
|
-
PIPE = 80,// |
|
|
196
197
|
ADDITIVE = 90,// +, -
|
|
197
198
|
MULTIPLICATIVE = 100,// *, /, div, mod
|
|
198
199
|
UNARY = 110,// unary +, -, not
|
|
199
|
-
AS_IS = 120,// as, is
|
|
200
200
|
POSTFIX = 130,// []
|
|
201
201
|
DOT = 140
|
|
202
202
|
}
|
|
@@ -253,6 +253,7 @@ interface FunctionSignature {
|
|
|
253
253
|
typeReference?: boolean;
|
|
254
254
|
}>;
|
|
255
255
|
result: TypeInfo | 'inputType' | 'inputTypeSingleton' | 'parameterType';
|
|
256
|
+
variadic?: boolean;
|
|
256
257
|
}
|
|
257
258
|
interface FunctionDefinition {
|
|
258
259
|
name: string;
|
|
@@ -330,7 +331,7 @@ interface IdentifierNode extends BaseASTNode {
|
|
|
330
331
|
interface LiteralNode extends BaseASTNode {
|
|
331
332
|
type: NodeType.Literal;
|
|
332
333
|
value: any;
|
|
333
|
-
valueType: 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
|
|
334
|
+
valueType: 'string' | 'number' | 'decimal' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
|
|
334
335
|
}
|
|
335
336
|
interface TemporalLiteralNode extends BaseASTNode {
|
|
336
337
|
type: NodeType.TemporalLiteral;
|
|
@@ -396,8 +397,8 @@ interface RuntimeContext {
|
|
|
396
397
|
currentNode?: ASTNode;
|
|
397
398
|
modelProvider?: ModelProvider;
|
|
398
399
|
}
|
|
399
|
-
interface EvaluationResult {
|
|
400
|
-
value:
|
|
400
|
+
interface EvaluationResult<T = any> {
|
|
401
|
+
value: T[];
|
|
401
402
|
context: RuntimeContext;
|
|
402
403
|
}
|
|
403
404
|
declare enum DiagnosticSeverity {
|
|
@@ -613,7 +614,8 @@ declare class Interpreter {
|
|
|
613
614
|
inputType?: TypeInfo;
|
|
614
615
|
modelProvider?: ModelProvider;
|
|
615
616
|
now?: Date;
|
|
616
|
-
|
|
617
|
+
includeMetadata?: boolean;
|
|
618
|
+
}): Promise<EvaluationResult['value']>;
|
|
617
619
|
private getBooleanKind;
|
|
618
620
|
private boxBoolean;
|
|
619
621
|
private evaluateTemporalLiteral;
|
|
@@ -743,72 +745,6 @@ declare class Analyzer {
|
|
|
743
745
|
analyze(ast: ASTNode, userVariables?: Record<string, any>, inputType?: TypeInfo, options?: AnalyzerOptions): Promise<AnalysisResultWithCursor>;
|
|
744
746
|
}
|
|
745
747
|
|
|
746
|
-
interface FHIRModelContext {
|
|
747
|
-
path: string;
|
|
748
|
-
schemaHierarchy: FHIRSchema[];
|
|
749
|
-
isUnion?: boolean;
|
|
750
|
-
choices?: Array<{
|
|
751
|
-
type: TypeName;
|
|
752
|
-
code: string;
|
|
753
|
-
choiceName?: string;
|
|
754
|
-
schema?: FHIRSchema;
|
|
755
|
-
}>;
|
|
756
|
-
canonicalUrl?: string;
|
|
757
|
-
version?: string;
|
|
758
|
-
}
|
|
759
|
-
interface FHIRModelProviderConfig {
|
|
760
|
-
packages: Array<{
|
|
761
|
-
name: string;
|
|
762
|
-
version: string;
|
|
763
|
-
}>;
|
|
764
|
-
cacheDir?: string;
|
|
765
|
-
registryUrl?: string;
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* FHIR ModelProvider implementation
|
|
769
|
-
*
|
|
770
|
-
* Note: This provider requires async initialization before use.
|
|
771
|
-
* Call initialize() before using the synchronous methods.
|
|
772
|
-
*
|
|
773
|
-
* For best performance, pre-load common types during initialization.
|
|
774
|
-
*/
|
|
775
|
-
declare class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
776
|
-
private config;
|
|
777
|
-
private canonicalManager;
|
|
778
|
-
private schemaCache;
|
|
779
|
-
private hierarchyCache;
|
|
780
|
-
private initialized;
|
|
781
|
-
private complexTypesCache?;
|
|
782
|
-
private primitiveTypesCache?;
|
|
783
|
-
private resourceTypesCache?;
|
|
784
|
-
private readonly typeMapping;
|
|
785
|
-
private readonly primitiveTypeMapping;
|
|
786
|
-
constructor(config?: FHIRModelProviderConfig);
|
|
787
|
-
initialize(): Promise<void>;
|
|
788
|
-
private buildCanonicalUrl;
|
|
789
|
-
getSchema(typeName: string): Promise<FHIRSchema | undefined>;
|
|
790
|
-
private getSchemaHierarchyAsync;
|
|
791
|
-
private extractTypeName;
|
|
792
|
-
private getSchemaHierarchyCached;
|
|
793
|
-
private mapToFHIRPathType;
|
|
794
|
-
private isChoiceType;
|
|
795
|
-
private createUnionContext;
|
|
796
|
-
getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
797
|
-
getElementType(parentType: TypeInfo<FHIRModelContext>, propertyName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
798
|
-
ofType(type: TypeInfo<FHIRModelContext>, typeName: TypeName): TypeInfo<FHIRModelContext> | undefined;
|
|
799
|
-
getElementNames(parentType: TypeInfo<FHIRModelContext>): string[];
|
|
800
|
-
getChildrenType(parentType: TypeInfo<FHIRModelContext>): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
801
|
-
loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
802
|
-
getElements(typeName: string): Promise<Array<{
|
|
803
|
-
name: string;
|
|
804
|
-
type: string;
|
|
805
|
-
documentation?: string;
|
|
806
|
-
}>>;
|
|
807
|
-
getResourceTypes(): Promise<string[]>;
|
|
808
|
-
getComplexTypes(): Promise<string[]>;
|
|
809
|
-
getPrimitiveTypes(): Promise<string[]>;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
748
|
interface ASTMetadata {
|
|
813
749
|
complexity: number;
|
|
814
750
|
depth: number;
|
|
@@ -895,6 +831,7 @@ declare const Errors: {
|
|
|
895
831
|
incompatibleUnits(unit1: string, unit2: string, location?: Range): FHIRPathError;
|
|
896
832
|
indexOutOfBounds(index: number, size: number, location?: Range): FHIRPathError;
|
|
897
833
|
invalidOperation(details: string, location?: Range): FHIRPathError;
|
|
834
|
+
systemVariableOverride(name: string, location?: Range): FHIRPathError;
|
|
898
835
|
invalidPrecision(operation: string, location?: Range): FHIRPathError;
|
|
899
836
|
invalidStringOperation(operation: string, paramName: string, location?: Range): FHIRPathError;
|
|
900
837
|
invalidNumericOperation(operation: string, paramName: string, expectedType: string, location?: Range): FHIRPathError;
|
|
@@ -995,13 +932,83 @@ interface EvaluateOptions {
|
|
|
995
932
|
variables?: Record<string, unknown>;
|
|
996
933
|
modelProvider?: ModelProvider;
|
|
997
934
|
inputType?: TypeInfo;
|
|
935
|
+
includeMetadata?: boolean;
|
|
998
936
|
}
|
|
999
|
-
declare function evaluate(expression: string, options?: EvaluateOptions): Promise<
|
|
937
|
+
declare function evaluate<T = any>(expression: string, options?: EvaluateOptions): Promise<EvaluationResult<T>['value']>;
|
|
1000
938
|
declare function analyze(expression: string, options?: {
|
|
1001
939
|
variables?: Record<string, unknown>;
|
|
1002
940
|
modelProvider?: ModelProvider;
|
|
1003
941
|
inputType?: TypeInfo;
|
|
1004
942
|
errorRecovery?: boolean;
|
|
1005
943
|
}): Promise<AnalysisResult>;
|
|
944
|
+
declare function getVersion(): string;
|
|
945
|
+
|
|
946
|
+
type Resource = {
|
|
947
|
+
url?: string;
|
|
948
|
+
version?: string;
|
|
949
|
+
id: string;
|
|
950
|
+
resourceType: string;
|
|
951
|
+
[key: string]: any;
|
|
952
|
+
};
|
|
953
|
+
interface FHIRModelContext {
|
|
954
|
+
path: string;
|
|
955
|
+
schemaHierarchy: FHIRSchema[];
|
|
956
|
+
isUnion?: boolean;
|
|
957
|
+
choices?: Array<{
|
|
958
|
+
type: TypeName;
|
|
959
|
+
code: string;
|
|
960
|
+
choiceName?: string;
|
|
961
|
+
schema?: FHIRSchema;
|
|
962
|
+
}>;
|
|
963
|
+
canonicalUrl?: string;
|
|
964
|
+
version?: string;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* FHIR ModelProvider implementation
|
|
968
|
+
*
|
|
969
|
+
* Note: This provider requires async initialization before use.
|
|
970
|
+
* Call initialize() before using the synchronous methods.
|
|
971
|
+
*
|
|
972
|
+
* For best performance, pre-load common types during initialization.
|
|
973
|
+
*/
|
|
974
|
+
declare class FHIRModelProviderBase implements ModelProvider<FHIRModelContext> {
|
|
975
|
+
private schemaCache;
|
|
976
|
+
private hierarchyCache;
|
|
977
|
+
private initialized;
|
|
978
|
+
private complexTypesCache?;
|
|
979
|
+
private primitiveTypesCache?;
|
|
980
|
+
private resourceTypesCache?;
|
|
981
|
+
private readonly typeMapping;
|
|
982
|
+
private readonly primitiveTypeMapping;
|
|
983
|
+
constructor();
|
|
984
|
+
prepare(): Promise<void>;
|
|
985
|
+
initialize(): Promise<void>;
|
|
986
|
+
resolve(_canonicalUrl: string): Promise<Resource | null>;
|
|
987
|
+
search(_params: {
|
|
988
|
+
kind: "primitive-type" | "complex-type" | "resource";
|
|
989
|
+
}): Promise<Resource[]>;
|
|
990
|
+
private buildCanonicalUrl;
|
|
991
|
+
getSchema(typeName: string): Promise<FHIRSchema | undefined>;
|
|
992
|
+
private getSchemaHierarchyAsync;
|
|
993
|
+
private extractTypeName;
|
|
994
|
+
private getSchemaHierarchyCached;
|
|
995
|
+
private mapToFHIRPathType;
|
|
996
|
+
private isChoiceType;
|
|
997
|
+
private createUnionContext;
|
|
998
|
+
getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
999
|
+
getElementType(parentType: TypeInfo<FHIRModelContext>, propertyName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
1000
|
+
ofType(type: TypeInfo<FHIRModelContext>, typeName: TypeName): TypeInfo<FHIRModelContext> | undefined;
|
|
1001
|
+
getElementNames(parentType: TypeInfo<FHIRModelContext>): string[];
|
|
1002
|
+
getChildrenType(parentType: TypeInfo<FHIRModelContext>): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
1003
|
+
loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
|
|
1004
|
+
getElements(typeName: string): Promise<Array<{
|
|
1005
|
+
name: string;
|
|
1006
|
+
type: string;
|
|
1007
|
+
documentation?: string;
|
|
1008
|
+
}>>;
|
|
1009
|
+
getResourceTypes(): Promise<string[]>;
|
|
1010
|
+
getComplexTypes(): Promise<string[]>;
|
|
1011
|
+
getPrimitiveTypes(): Promise<string[]>;
|
|
1012
|
+
}
|
|
1006
1013
|
|
|
1007
|
-
export {
|
|
1014
|
+
export { Analyzer as A, type CursorNode as B, CompletionKind as C, DiagnosticSeverity as D, type EvaluateOptions as E, FHIRModelProviderBase as F, type CursorOperatorNode as G, type CursorIdentifierNode as H, Interpreter as I, type CursorArgumentNode as J, type CursorIndexNode as K, type CursorTypeNode as L, type ModelProvider as M, type AnyCursorNode as N, type OperatorDefinition as O, Parser as P, type Resource as R, type TypeInfo as T, type FHIRModelContext as a, analyze as b, Registry as c, type ParseResult as d, evaluate as e, type Diagnostic as f, getVersion as g, type AnalysisResult as h, type ASTNode as i, type TypeName as j, type FunctionDefinition as k, type EvaluationResult as l, inspect as m, type InspectOptions as n, type InspectResult as o, parse as p, type ASTMetadata as q, registry as r, FHIRPathError as s, Errors as t, ErrorCodes as u, provideCompletions as v, type CompletionItem as w, type CompletionOptions as x, CursorContext as y, isCursorNode as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomic-ehr/fhirpath",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "A TypeScript implementation of FHIRPath",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"
|
|
12
|
+
"node": "./dist/index.node.js",
|
|
13
|
+
"default": "./dist/index.browser.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
"@types/lodash": "^4.17.20",
|
|
49
50
|
"@types/node": "22.13.14",
|
|
50
51
|
"@types/turndown": "^5.0.5",
|
|
52
|
+
"@types/xml2js": "^0.4.14",
|
|
53
|
+
"@xmldom/xmldom": "^0.8.11",
|
|
51
54
|
"antlr4ts": "^0.5.0-alpha.4",
|
|
52
55
|
"antlr4ts-cli": "^0.5.0-alpha.4",
|
|
53
56
|
"js-yaml": "^4.1.0",
|
|
@@ -56,7 +59,8 @@
|
|
|
56
59
|
"tsup": "8.5.0",
|
|
57
60
|
"turndown": "^7.2.0",
|
|
58
61
|
"typescript": "^5.8.3",
|
|
59
|
-
"typescript-mcp": "^0.0.14"
|
|
62
|
+
"typescript-mcp": "^0.0.14",
|
|
63
|
+
"xml2js": "^0.6.2"
|
|
60
64
|
},
|
|
61
65
|
"peerDependencies": {
|
|
62
66
|
"typescript": "^5"
|
|
@@ -64,6 +68,7 @@
|
|
|
64
68
|
"dependencies": {
|
|
65
69
|
"@atomic-ehr/fhir-canonical-manager": "^0.0.11",
|
|
66
70
|
"@atomic-ehr/fhirschema": "^0.0.2",
|
|
67
|
-
"@atomic-ehr/ucum": "^0.2.5"
|
|
71
|
+
"@atomic-ehr/ucum": "^0.2.5",
|
|
72
|
+
"fast-xml-parser": "^5.2.5"
|
|
68
73
|
}
|
|
69
|
-
}
|
|
74
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -198,6 +198,19 @@ export class Analyzer {
|
|
|
198
198
|
|
|
199
199
|
// Special handling for dot operator - flow context through
|
|
200
200
|
if (node.operator === '.') {
|
|
201
|
+
// Check if this is actually a namespaced type in an 'is' expression
|
|
202
|
+
// Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
|
|
203
|
+
if (node.left.type === NodeType.MembershipTest && node.right.type === NodeType.Identifier) {
|
|
204
|
+
const membershipTest = node.left as MembershipTestNode;
|
|
205
|
+
const rightIdent = node.right as IdentifierNode;
|
|
206
|
+
// Reconstruct the correct MembershipTest with full type name
|
|
207
|
+
const correctedNode: MembershipTestNode = {
|
|
208
|
+
...membershipTest,
|
|
209
|
+
targetType: `${membershipTest.targetType}.${rightIdent.name}`
|
|
210
|
+
};
|
|
211
|
+
return await this.analyzeMembershipTest(correctedNode, context);
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
const leftResult = await this.analyzeNode(node.left, context);
|
|
202
215
|
if (this.stoppedAtCursor) {
|
|
203
216
|
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
@@ -391,7 +404,7 @@ export class Analyzer {
|
|
|
391
404
|
if (signature) {
|
|
392
405
|
const params = signature.parameters || [];
|
|
393
406
|
const requiredCount = params.filter(p => !p.optional).length;
|
|
394
|
-
const maxCount = params.length;
|
|
407
|
+
const maxCount = signature.variadic ? Infinity : params.length;
|
|
395
408
|
const actualCount = node.arguments.length;
|
|
396
409
|
|
|
397
410
|
if (actualCount < requiredCount) {
|
|
@@ -468,6 +481,7 @@ export class Analyzer {
|
|
|
468
481
|
if (param?.expression) {
|
|
469
482
|
const itemType = { ...context.inputType, singleton: true };
|
|
470
483
|
const exprContext = context
|
|
484
|
+
.withInputType(itemType) // Set input type to singleton for expression parameters
|
|
471
485
|
.withSystemVariable('$this', itemType)
|
|
472
486
|
.withSystemVariable('$index', { type: 'Integer', singleton: true });
|
|
473
487
|
const argResult = await this.analyzeNode(arg, exprContext);
|
|
@@ -637,15 +651,29 @@ export class Analyzer {
|
|
|
637
651
|
|
|
638
652
|
// Check if it's a user variable (starts with %)
|
|
639
653
|
if (varName.startsWith('%')) {
|
|
640
|
-
// Special handling for
|
|
641
|
-
// that always returns the original input to the evaluation engine
|
|
654
|
+
// Special handling for built-in environment variables
|
|
642
655
|
if (varName === '%context') {
|
|
643
656
|
// %context returns the root input type (the original input to evaluate())
|
|
644
657
|
// In the analyzer, we track this as the initial input type
|
|
645
658
|
return { type: context.inputType, diagnostics, context };
|
|
646
659
|
}
|
|
647
660
|
|
|
648
|
-
|
|
661
|
+
// Special handling for FHIR system variables (code system URLs)
|
|
662
|
+
if (varName === '%sct' || varName === '%loinc' || varName === '%ucum' ||
|
|
663
|
+
varName === '%vs-administrative-gender' || varName === '%`vs-administrative-gender`') {
|
|
664
|
+
// These are string constants
|
|
665
|
+
return { type: { type: 'String', singleton: true }, diagnostics, context };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Handle environment variable syntax with backticks %`variable`
|
|
669
|
+
let name: string;
|
|
670
|
+
if (varName.startsWith('%`') && varName.endsWith('`')) {
|
|
671
|
+
// Remove %` from start and ` from end
|
|
672
|
+
name = varName.slice(2, -1);
|
|
673
|
+
} else {
|
|
674
|
+
// Remove % prefix
|
|
675
|
+
name = varName.substring(1);
|
|
676
|
+
}
|
|
649
677
|
const varType = context.userVariables.get(name);
|
|
650
678
|
|
|
651
679
|
if (!varType) {
|
|
@@ -745,10 +773,12 @@ export class Analyzer {
|
|
|
745
773
|
type = { type: 'String', singleton: true };
|
|
746
774
|
break;
|
|
747
775
|
case 'number':
|
|
748
|
-
//
|
|
749
|
-
type =
|
|
750
|
-
|
|
751
|
-
|
|
776
|
+
// Number without decimal point is integer
|
|
777
|
+
type = { type: 'Integer', singleton: true };
|
|
778
|
+
break;
|
|
779
|
+
case 'decimal':
|
|
780
|
+
// Number with decimal point is decimal (even if value is whole)
|
|
781
|
+
type = { type: 'Decimal', singleton: true };
|
|
752
782
|
break;
|
|
753
783
|
case 'boolean':
|
|
754
784
|
type = { type: 'Boolean', singleton: true };
|
|
@@ -890,7 +920,14 @@ export class Analyzer {
|
|
|
890
920
|
|
|
891
921
|
// ModelProvider requirement for non-primitive target types
|
|
892
922
|
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
893
|
-
|
|
923
|
+
|
|
924
|
+
// Normalize System.X types to check if they're primitive
|
|
925
|
+
let targetType = node.targetType;
|
|
926
|
+
if (targetType.startsWith('System.')) {
|
|
927
|
+
targetType = targetType.substring(7); // Remove "System." prefix
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (!context.modelProvider && !primitiveTypes.includes(targetType)) {
|
|
894
931
|
diagnostics.push(toDiagnostic(Errors.modelProviderRequired('is', node.range)));
|
|
895
932
|
}
|
|
896
933
|
|
|
@@ -25,6 +25,29 @@ export const CALENDAR_DURATION_UNITS = new Set([
|
|
|
25
25
|
'millisecond', 'milliseconds'
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Calendar to UCUM duration mappings
|
|
30
|
+
* Maps FHIRPath calendar duration units to their UCUM equivalents
|
|
31
|
+
*/
|
|
32
|
+
export const CALENDAR_TO_UCUM: Record<string, string> = {
|
|
33
|
+
'year': 'a',
|
|
34
|
+
'years': 'a',
|
|
35
|
+
'month': 'mo',
|
|
36
|
+
'months': 'mo',
|
|
37
|
+
'week': 'wk',
|
|
38
|
+
'weeks': 'wk',
|
|
39
|
+
'day': 'd',
|
|
40
|
+
'days': 'd',
|
|
41
|
+
'hour': 'h',
|
|
42
|
+
'hours': 'h',
|
|
43
|
+
'minute': 'min',
|
|
44
|
+
'minutes': 'min',
|
|
45
|
+
'second': 's',
|
|
46
|
+
'seconds': 's',
|
|
47
|
+
'millisecond': 'ms',
|
|
48
|
+
'milliseconds': 'ms'
|
|
49
|
+
};
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
52
|
* Create a quantity value
|
|
30
53
|
*/
|
|
@@ -234,19 +257,118 @@ export function divideQuantities(left: QuantityValue, right: QuantityValue): Qua
|
|
|
234
257
|
* Returns -1 if left < right, 0 if equal, 1 if left > right, null if incomparable
|
|
235
258
|
*/
|
|
236
259
|
export function compareQuantities(left: QuantityValue, right: QuantityValue): number | null {
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
260
|
+
// Handle calendar to UCUM comparisons
|
|
261
|
+
const leftIsCalendar = CALENDAR_DURATION_UNITS.has(left.unit);
|
|
262
|
+
const rightIsCalendar = CALENDAR_DURATION_UNITS.has(right.unit);
|
|
263
|
+
|
|
264
|
+
// Both calendar units - only compare if they're compatible units
|
|
265
|
+
if (leftIsCalendar && rightIsCalendar) {
|
|
266
|
+
// Only allow conversion between specific compatible units
|
|
267
|
+
const areCompatible = (unit1: string, unit2: string): boolean => {
|
|
268
|
+
// Normalize to singular form
|
|
269
|
+
const normalize = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
|
|
270
|
+
const n1 = normalize(unit1);
|
|
271
|
+
const n2 = normalize(unit2);
|
|
272
|
+
|
|
273
|
+
// Same unit (singular/plural)
|
|
274
|
+
if (n1 === n2) return true;
|
|
275
|
+
|
|
276
|
+
// Week <-> days conversion
|
|
277
|
+
if ((n1 === 'week' && n2 === 'day') || (n1 === 'day' && n2 === 'week')) return true;
|
|
278
|
+
|
|
279
|
+
// No other conversions between calendar units
|
|
280
|
+
return false;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (!areCompatible(left.unit, right.unit)) {
|
|
284
|
+
// Different calendar units that aren't compatible
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle week/day conversion
|
|
289
|
+
const normalizeToSingular = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
|
|
290
|
+
const leftNorm = normalizeToSingular(left.unit);
|
|
291
|
+
const rightNorm = normalizeToSingular(right.unit);
|
|
292
|
+
|
|
293
|
+
if (leftNorm === rightNorm) {
|
|
294
|
+
// Same unit, just compare values
|
|
295
|
+
if (left.value < right.value) return -1;
|
|
296
|
+
if (left.value > right.value) return 1;
|
|
244
297
|
return 0;
|
|
245
298
|
}
|
|
299
|
+
|
|
300
|
+
// Week <-> day conversion
|
|
301
|
+
if ((leftNorm === 'week' && rightNorm === 'day') ||
|
|
302
|
+
(leftNorm === 'day' && rightNorm === 'week')) {
|
|
303
|
+
const leftInDays = leftNorm === 'week' ? left.value * 7 : left.value;
|
|
304
|
+
const rightInDays = rightNorm === 'week' ? right.value * 7 : right.value;
|
|
305
|
+
|
|
306
|
+
if (leftInDays < rightInDays) return -1;
|
|
307
|
+
if (leftInDays > rightInDays) return 1;
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Shouldn't reach here
|
|
312
|
+
return null;
|
|
246
313
|
}
|
|
247
314
|
|
|
248
|
-
//
|
|
249
|
-
if (
|
|
315
|
+
// Calendar to UCUM comparison
|
|
316
|
+
if (leftIsCalendar && !rightIsCalendar) {
|
|
317
|
+
// Check for week/day special case
|
|
318
|
+
if ((left.unit === 'days' || left.unit === 'day') && right.unit === 'wk') {
|
|
319
|
+
// Convert days to weeks
|
|
320
|
+
const leftInWeeks = left.value / 7;
|
|
321
|
+
if (leftInWeeks < right.value) return -1;
|
|
322
|
+
if (leftInWeeks > right.value) return 1;
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
if ((left.unit === 'weeks' || left.unit === 'week') && right.unit === 'd') {
|
|
326
|
+
// Convert weeks to days
|
|
327
|
+
const leftInDays = left.value * 7;
|
|
328
|
+
if (leftInDays < right.value) return -1;
|
|
329
|
+
if (leftInDays > right.value) return 1;
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Direct mapping check
|
|
334
|
+
const leftUcumUnit = CALENDAR_TO_UCUM[left.unit];
|
|
335
|
+
if (leftUcumUnit === right.unit) {
|
|
336
|
+
// Direct mapping, compare values
|
|
337
|
+
if (left.value < right.value) return -1;
|
|
338
|
+
if (left.value > right.value) return 1;
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
// No mapping, incomparable
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// UCUM to calendar comparison
|
|
346
|
+
if (!leftIsCalendar && rightIsCalendar) {
|
|
347
|
+
// Check for week/day special case
|
|
348
|
+
if (left.unit === 'wk' && (right.unit === 'days' || right.unit === 'day')) {
|
|
349
|
+
// Convert weeks to days
|
|
350
|
+
const leftInDays = left.value * 7;
|
|
351
|
+
if (leftInDays < right.value) return -1;
|
|
352
|
+
if (leftInDays > right.value) return 1;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
if (left.unit === 'd' && (right.unit === 'weeks' || right.unit === 'week')) {
|
|
356
|
+
// Convert days to weeks
|
|
357
|
+
const leftInWeeks = left.value / 7;
|
|
358
|
+
if (leftInWeeks < right.value) return -1;
|
|
359
|
+
if (leftInWeeks > right.value) return 1;
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Direct mapping check
|
|
364
|
+
const rightUcumUnit = CALENDAR_TO_UCUM[right.unit];
|
|
365
|
+
if (left.unit === rightUcumUnit) {
|
|
366
|
+
// Direct mapping, compare values
|
|
367
|
+
if (left.value < right.value) return -1;
|
|
368
|
+
if (left.value > right.value) return 1;
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
// No mapping, incomparable
|
|
250
372
|
return null;
|
|
251
373
|
}
|
|
252
374
|
|
|
@@ -461,7 +461,33 @@ export function equivalent(a: TemporalValue, b: TemporalValue): boolean {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
export function compare(a: TemporalValue, b: TemporalValue): -1 | 0 | 1 | null {
|
|
464
|
-
//
|
|
464
|
+
// Special case: DateTime and Date can be compared when date portions differ
|
|
465
|
+
if (isFHIRDateTime(a) && isFHIRDate(b)) {
|
|
466
|
+
// Compare the date portion of DateTime with Date
|
|
467
|
+
if (a.year !== b.year) return a.year < b.year ? -1 : 1;
|
|
468
|
+
if (b.month !== undefined && a.month !== undefined) {
|
|
469
|
+
if (a.month !== b.month) return a.month < b.month ? -1 : 1;
|
|
470
|
+
}
|
|
471
|
+
if (b.day !== undefined && a.day !== undefined) {
|
|
472
|
+
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
|
473
|
+
}
|
|
474
|
+
// When date portions are equal, Date and DateTime are incomparable
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (isFHIRDate(a) && isFHIRDateTime(b)) {
|
|
478
|
+
// Compare Date with the date portion of DateTime
|
|
479
|
+
if (a.year !== b.year) return a.year < b.year ? -1 : 1;
|
|
480
|
+
if (a.month !== undefined && b.month !== undefined) {
|
|
481
|
+
if (a.month !== b.month) return a.month < b.month ? -1 : 1;
|
|
482
|
+
}
|
|
483
|
+
if (a.day !== undefined && b.day !== undefined) {
|
|
484
|
+
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
|
485
|
+
}
|
|
486
|
+
// When date portions are equal, Date and DateTime are incomparable
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Different types (except DateTime/Date) can't be compared
|
|
465
491
|
if (a.kind !== b.kind) {
|
|
466
492
|
return null;
|
|
467
493
|
}
|
|
@@ -765,7 +791,7 @@ function parseDateLiteral(value: string): FHIRDate {
|
|
|
765
791
|
}
|
|
766
792
|
|
|
767
793
|
function parseTimeLiteral(value: string): FHIRTime {
|
|
768
|
-
const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d
|
|
794
|
+
const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
|
|
769
795
|
if (!match) {
|
|
770
796
|
throw new Error(`Invalid time literal: @T${value}`);
|
|
771
797
|
}
|
|
@@ -773,7 +799,14 @@ function parseTimeLiteral(value: string): FHIRTime {
|
|
|
773
799
|
const hour = parseInt(match[1]!, 10);
|
|
774
800
|
const minute = match[2] ? parseInt(match[2]!, 10) : undefined;
|
|
775
801
|
const second = match[3] ? parseInt(match[3]!, 10) : undefined;
|
|
776
|
-
|
|
802
|
+
// Handle variable-length fractional seconds (pad or truncate to 3 digits)
|
|
803
|
+
let millisecond: number | undefined;
|
|
804
|
+
if (match[4]) {
|
|
805
|
+
const fraction = match[4];
|
|
806
|
+
// Pad to 3 digits if needed, truncate if longer
|
|
807
|
+
const padded = (fraction + '000').substring(0, 3);
|
|
808
|
+
millisecond = parseInt(padded, 10);
|
|
809
|
+
}
|
|
777
810
|
|
|
778
811
|
return createTime(hour, minute, second, millisecond);
|
|
779
812
|
}
|
|
@@ -826,13 +859,19 @@ function parseDateTimeLiteral(value: string): FHIRDateTime {
|
|
|
826
859
|
}
|
|
827
860
|
}
|
|
828
861
|
|
|
829
|
-
// Parse time components
|
|
830
|
-
const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d
|
|
862
|
+
// Parse time components - handle variable-length milliseconds
|
|
863
|
+
const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
|
|
831
864
|
if (timeMatch) {
|
|
832
865
|
hour = parseInt(timeMatch[1]!, 10);
|
|
833
866
|
minute = timeMatch[2] ? parseInt(timeMatch[2]!, 10) : undefined;
|
|
834
867
|
second = timeMatch[3] ? parseInt(timeMatch[3]!, 10) : undefined;
|
|
835
|
-
|
|
868
|
+
// Handle variable-length fractional seconds (pad or truncate to 3 digits)
|
|
869
|
+
if (timeMatch[4]) {
|
|
870
|
+
const fraction = timeMatch[4];
|
|
871
|
+
// Pad to 3 digits if needed, truncate if longer
|
|
872
|
+
const padded = (fraction + '000').substring(0, 3);
|
|
873
|
+
millisecond = parseInt(padded, 10);
|
|
874
|
+
}
|
|
836
875
|
}
|
|
837
876
|
}
|
|
838
877
|
|
package/src/errors.ts
CHANGED
|
@@ -185,6 +185,10 @@ export const Errors = {
|
|
|
185
185
|
invalidOperation(details: string, location?: Range): FHIRPathError {
|
|
186
186
|
return new FHIRPathError(ErrorCodes.INVALID_OPERATION, `Invalid operation: ${details}`, location);
|
|
187
187
|
},
|
|
188
|
+
|
|
189
|
+
systemVariableOverride(name: string, location?: Range): FHIRPathError {
|
|
190
|
+
return new FHIRPathError(ErrorCodes.INVALID_OPERATION, `Cannot override system variable '${name}'`, location);
|
|
191
|
+
},
|
|
188
192
|
|
|
189
193
|
invalidPrecision(operation: string, location?: Range): FHIRPathError {
|
|
190
194
|
return new FHIRPathError(ErrorCodes.INVALID_PRECISION, `${operation} precision must be a non-negative integer`, location);
|