fhirsmith 0.3.0
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/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,2077 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SNOMED CT Expression Library
|
|
3
|
+
* Converted from Pascal ftx_sct_expressions.pas
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2011+, HL7 and Health Intersections Pty Ltd
|
|
6
|
+
* Licensed under BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MAX_TERM_LENGTH = 1024;
|
|
10
|
+
const NO_REFERENCE = 0xFFFFFFFF;
|
|
11
|
+
|
|
12
|
+
// Expression status enumeration
|
|
13
|
+
const SnomedExpressionStatus = {
|
|
14
|
+
Unknown: 0,
|
|
15
|
+
Equivalent: 1,
|
|
16
|
+
SubsumedBy: 2
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base class for all SNOMED expression elements
|
|
21
|
+
*/
|
|
22
|
+
class SnomedExpressionBase {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.start = 0;
|
|
25
|
+
this.stop = 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Represents a SNOMED concept with optional code, description, literal, or decimal value
|
|
31
|
+
*/
|
|
32
|
+
class SnomedConcept extends SnomedExpressionBase {
|
|
33
|
+
constructor(reference = NO_REFERENCE) {
|
|
34
|
+
super();
|
|
35
|
+
this.reference = reference;
|
|
36
|
+
this.code = '';
|
|
37
|
+
this.description = '';
|
|
38
|
+
this.literal = '';
|
|
39
|
+
this.decimal = '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if this concept matches another concept
|
|
44
|
+
*/
|
|
45
|
+
matches(other) {
|
|
46
|
+
if (!other) return false;
|
|
47
|
+
|
|
48
|
+
if (this.reference !== NO_REFERENCE) {
|
|
49
|
+
return this.reference === other.reference;
|
|
50
|
+
} else if (this.code) {
|
|
51
|
+
return this.code === other.code;
|
|
52
|
+
} else if (this.decimal) {
|
|
53
|
+
return this.decimal === other.decimal;
|
|
54
|
+
} else if (this.literal) {
|
|
55
|
+
return this.literal === other.literal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a string description of this concept
|
|
63
|
+
*/
|
|
64
|
+
describe() {
|
|
65
|
+
if (this.code) {
|
|
66
|
+
return this.code;
|
|
67
|
+
} else if (this.decimal) {
|
|
68
|
+
return '#' + this.decimal;
|
|
69
|
+
} else if (this.literal) {
|
|
70
|
+
return '"' + this.literal + '"';
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compare two concepts for sorting
|
|
77
|
+
*/
|
|
78
|
+
compare(other) {
|
|
79
|
+
if (this.code) {
|
|
80
|
+
return this.code.localeCompare(other.code);
|
|
81
|
+
} else if (this.decimal) {
|
|
82
|
+
return this.decimal.localeCompare(other.decimal);
|
|
83
|
+
} else {
|
|
84
|
+
return this.literal.localeCompare(other.literal);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a canonical copy of this concept
|
|
90
|
+
*/
|
|
91
|
+
canonical() {
|
|
92
|
+
const result = new SnomedConcept();
|
|
93
|
+
result.copyFrom(this);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Copy properties from another concept
|
|
99
|
+
*/
|
|
100
|
+
copyFrom(other) {
|
|
101
|
+
this.reference = other.reference;
|
|
102
|
+
this.code = other.code;
|
|
103
|
+
this.description = other.description;
|
|
104
|
+
this.literal = other.literal;
|
|
105
|
+
this.decimal = other.decimal;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Represents a refinement (attribute-value pair) in a SNOMED expression
|
|
111
|
+
*/
|
|
112
|
+
class SnomedRefinement extends SnomedExpressionBase {
|
|
113
|
+
constructor() {
|
|
114
|
+
super();
|
|
115
|
+
this.name = null; // SnomedConcept
|
|
116
|
+
this.value = null; // SnomedExpression
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set the name (attribute) of this refinement
|
|
121
|
+
*/
|
|
122
|
+
setName(name) {
|
|
123
|
+
this.name = name;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set the value of this refinement
|
|
128
|
+
*/
|
|
129
|
+
setValue(value) {
|
|
130
|
+
this.value = value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if this refinement matches another
|
|
135
|
+
*/
|
|
136
|
+
matches(other) {
|
|
137
|
+
if (!other) return false;
|
|
138
|
+
if (!this.name.matches(other.name)) return false;
|
|
139
|
+
return this.value.matches(other.value) === '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a string description of this refinement
|
|
144
|
+
*/
|
|
145
|
+
describe() {
|
|
146
|
+
return this.name.describe() + '=' + this.value.describe();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compare two refinements for sorting
|
|
151
|
+
*/
|
|
152
|
+
compare(other) {
|
|
153
|
+
return this.name.compare(other.name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a canonical copy of this refinement
|
|
158
|
+
*/
|
|
159
|
+
canonical() {
|
|
160
|
+
const result = new SnomedRefinement();
|
|
161
|
+
result.name = this.name.canonical();
|
|
162
|
+
result.value = this.value.canonical();
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Represents a group of refinements in a SNOMED expression
|
|
169
|
+
*/
|
|
170
|
+
class SnomedRefinementGroup extends SnomedExpressionBase {
|
|
171
|
+
constructor() {
|
|
172
|
+
super();
|
|
173
|
+
this.refinements = []; // Array of SnomedRefinement
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if this group matches another group
|
|
178
|
+
*/
|
|
179
|
+
matches(other) {
|
|
180
|
+
if (!other) return false;
|
|
181
|
+
|
|
182
|
+
// Check all refinements in this group exist in other
|
|
183
|
+
for (const refinement of this.refinements) {
|
|
184
|
+
if (!other.hasRefinement(refinement)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check all refinements in other group exist in this
|
|
190
|
+
for (const refinement of other.refinements) {
|
|
191
|
+
if (!this.hasRefinement(refinement)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if this group contains a specific refinement
|
|
201
|
+
*/
|
|
202
|
+
hasRefinement(refinement) {
|
|
203
|
+
return this.refinements.some(test => test.matches(refinement));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get a string description of this refinement group
|
|
208
|
+
*/
|
|
209
|
+
describe() {
|
|
210
|
+
return this.refinements.map(r => r.describe()).join(',');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Compare two refinement groups for sorting
|
|
215
|
+
*/
|
|
216
|
+
compare(other) {
|
|
217
|
+
if (this.refinements.length === 0 || other.refinements.length === 0) {
|
|
218
|
+
return this.refinements.length - other.refinements.length;
|
|
219
|
+
}
|
|
220
|
+
return this.refinements[0].compare(other.refinements[0]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a canonical copy of this refinement group
|
|
225
|
+
*/
|
|
226
|
+
canonical() {
|
|
227
|
+
const result = new SnomedRefinementGroup();
|
|
228
|
+
for (const refinement of this.refinements) {
|
|
229
|
+
result.refinements.push(refinement.canonical());
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Main SNOMED expression class containing concepts, refinements, and refinement groups
|
|
237
|
+
*/
|
|
238
|
+
class SnomedExpression extends SnomedExpressionBase {
|
|
239
|
+
constructor() {
|
|
240
|
+
super();
|
|
241
|
+
this.status = SnomedExpressionStatus.Unknown;
|
|
242
|
+
this.concepts = []; // Array of SnomedConcept
|
|
243
|
+
this.refinements = []; // Array of SnomedRefinement
|
|
244
|
+
this.refinementGroups = []; // Array of SnomedRefinementGroup
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if expression has refinements
|
|
249
|
+
*/
|
|
250
|
+
hasRefinements() {
|
|
251
|
+
return this.refinements.length > 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if expression has refinement groups
|
|
256
|
+
*/
|
|
257
|
+
hasRefinementGroups() {
|
|
258
|
+
return this.refinementGroups.length > 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if this is a simple expression (single concept, no refinements)
|
|
263
|
+
*/
|
|
264
|
+
isSimple() {
|
|
265
|
+
return (this.concepts.length === 1) &&
|
|
266
|
+
!this.hasRefinements() &&
|
|
267
|
+
!this.hasRefinementGroups();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if this is a complex expression
|
|
272
|
+
*/
|
|
273
|
+
isComplex() {
|
|
274
|
+
return !this.isSimple();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if expression contains a specific concept
|
|
279
|
+
*/
|
|
280
|
+
hasConcept(concept) {
|
|
281
|
+
if (typeof concept === 'number') {
|
|
282
|
+
return this.concepts.some(test => test.reference === concept);
|
|
283
|
+
} else {
|
|
284
|
+
return this.concepts.some(test => test.matches(concept));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if expression contains a specific refinement
|
|
290
|
+
*/
|
|
291
|
+
hasRefinement(refinement) {
|
|
292
|
+
return this.refinements.some(test => test.matches(refinement));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if expression contains a specific refinement group
|
|
297
|
+
*/
|
|
298
|
+
hasRefinementGroup(refinementGroup) {
|
|
299
|
+
return this.refinementGroups.some(test => test.matches(refinementGroup));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if this expression matches another expression
|
|
304
|
+
* Returns empty string if match, error message if not
|
|
305
|
+
*/
|
|
306
|
+
matches(other) {
|
|
307
|
+
if (!other) return 'other is nil';
|
|
308
|
+
|
|
309
|
+
// Check concepts
|
|
310
|
+
for (const concept of this.concepts) {
|
|
311
|
+
if (!other.hasConcept(concept)) {
|
|
312
|
+
return 'concept ' + concept.describe() + ' not found in first expression';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const concept of other.concepts) {
|
|
316
|
+
if (!this.hasConcept(concept)) {
|
|
317
|
+
return 'concept ' + concept.describe() + ' not found in second expression';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check refinements
|
|
322
|
+
for (const refinement of this.refinements) {
|
|
323
|
+
if (!other.hasRefinement(refinement)) {
|
|
324
|
+
return 'refinement ' + refinement.describe() + ' not found in first expression';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const refinement of other.refinements) {
|
|
328
|
+
if (!this.hasRefinement(refinement)) {
|
|
329
|
+
return 'refinement ' + refinement.describe() + ' not found in second expression';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check refinement groups
|
|
334
|
+
for (const group of this.refinementGroups) {
|
|
335
|
+
if (!other.hasRefinementGroup(group)) {
|
|
336
|
+
return 'refinement group ' + group.describe() + ' not found in first expression';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const group of other.refinementGroups) {
|
|
340
|
+
if (!this.hasRefinementGroup(group)) {
|
|
341
|
+
return 'refinement group ' + group.describe() + ' not found in second expression';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get a string description of this expression
|
|
350
|
+
*/
|
|
351
|
+
describe() {
|
|
352
|
+
const parts = [];
|
|
353
|
+
|
|
354
|
+
// Status prefix
|
|
355
|
+
switch (this.status) {
|
|
356
|
+
case SnomedExpressionStatus.Equivalent:
|
|
357
|
+
parts.push('===');
|
|
358
|
+
break;
|
|
359
|
+
case SnomedExpressionStatus.SubsumedBy:
|
|
360
|
+
parts.push('<<<');
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Concepts
|
|
365
|
+
parts.push(...this.concepts.map(c => c.describe()));
|
|
366
|
+
|
|
367
|
+
// Refinements
|
|
368
|
+
parts.push(...this.refinements.map(r => r.describe()));
|
|
369
|
+
|
|
370
|
+
// Refinement groups
|
|
371
|
+
parts.push(...this.refinementGroups.map(g => g.describe()));
|
|
372
|
+
|
|
373
|
+
return parts.join(',');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Create a canonical form of this expression (sorted and normalized)
|
|
378
|
+
*/
|
|
379
|
+
canonical() {
|
|
380
|
+
const result = new SnomedExpression();
|
|
381
|
+
result.status = this.status;
|
|
382
|
+
|
|
383
|
+
// Copy and sort concepts
|
|
384
|
+
for (const concept of this.concepts) {
|
|
385
|
+
result.concepts.push(concept.canonical());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Convert refinements and groups to canonical form
|
|
389
|
+
if (this.hasRefinementGroups() || this.hasRefinements()) {
|
|
390
|
+
// Copy refinement groups
|
|
391
|
+
for (const group of this.refinementGroups) {
|
|
392
|
+
if (group.refinements.length > 0) {
|
|
393
|
+
result.refinementGroups.push(group.canonical());
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Convert individual refinements to single-refinement groups
|
|
398
|
+
for (const refinement of this.refinements) {
|
|
399
|
+
const group = new SnomedRefinementGroup();
|
|
400
|
+
group.refinements.push(refinement.canonical());
|
|
401
|
+
result.refinementGroups.push(group);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Sort everything
|
|
406
|
+
result.concepts.sort((a, b) => a.compare(b));
|
|
407
|
+
|
|
408
|
+
if (result.hasRefinementGroups()) {
|
|
409
|
+
for (const group of result.refinementGroups) {
|
|
410
|
+
group.refinements.sort((a, b) => a.compare(b));
|
|
411
|
+
}
|
|
412
|
+
result.refinementGroups.sort((a, b) => a.compare(b));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Merge another expression into this one
|
|
420
|
+
*/
|
|
421
|
+
merge(exp) {
|
|
422
|
+
if (exp) {
|
|
423
|
+
this.concepts.push(...exp.concepts);
|
|
424
|
+
this.refinements.push(...exp.refinements);
|
|
425
|
+
this.refinementGroups.push(...exp.refinementGroups);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parser for SNOMED CT expression strings
|
|
432
|
+
*/
|
|
433
|
+
class SnomedExpressionParser {
|
|
434
|
+
/**
|
|
435
|
+
* @param {SnomedConceptList} conceptList - list of all snomed concepts
|
|
436
|
+
*/
|
|
437
|
+
constructor(conceptList) {
|
|
438
|
+
this.source = '';
|
|
439
|
+
this.cursor = 0;
|
|
440
|
+
this.conceptList = conceptList;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Parse a SNOMED expression string
|
|
445
|
+
*/
|
|
446
|
+
parse(source) {
|
|
447
|
+
this.source = source;
|
|
448
|
+
this.cursor = 0;
|
|
449
|
+
|
|
450
|
+
const result = new SnomedExpression();
|
|
451
|
+
result.start = this.cursor;
|
|
452
|
+
|
|
453
|
+
this.ws();
|
|
454
|
+
|
|
455
|
+
// Check for status prefix
|
|
456
|
+
if (this.peek() === '=') {
|
|
457
|
+
result.status = SnomedExpressionStatus.Equivalent;
|
|
458
|
+
this.prefix('=');
|
|
459
|
+
} else if (this.peek() === '<') {
|
|
460
|
+
result.status = SnomedExpressionStatus.SubsumedBy;
|
|
461
|
+
this.prefix('<');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Parse main expression
|
|
465
|
+
result.concepts.push(this.concept());
|
|
466
|
+
while (this.gchar('+')) {
|
|
467
|
+
result.concepts.push(this.concept());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (this.gchar(':')) {
|
|
471
|
+
this.ws();
|
|
472
|
+
this.refinements(result);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
result.stop = this.cursor;
|
|
476
|
+
|
|
477
|
+
this.rule(this.peek() === '\0', 'Found content ("' + this.peekDisp() + '") after end of expression');
|
|
478
|
+
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Parse a concept
|
|
484
|
+
*/
|
|
485
|
+
concept() {
|
|
486
|
+
const result = new SnomedConcept();
|
|
487
|
+
result.start = this.cursor;
|
|
488
|
+
|
|
489
|
+
this.ws();
|
|
490
|
+
|
|
491
|
+
if (this.peek() === '#') {
|
|
492
|
+
result.decimal = this.decimal();
|
|
493
|
+
} else if (this.peek() === '"') {
|
|
494
|
+
result.literal = this.stringConstant();
|
|
495
|
+
} else {
|
|
496
|
+
result.code = this.conceptId();
|
|
497
|
+
if (this.conceptList) {
|
|
498
|
+
const found = this.conceptList.findConcept(result.code)
|
|
499
|
+
this.rule(found.found, 'Concept "' + result.code + '" not valid');
|
|
500
|
+
result.reference = found.index;
|
|
501
|
+
} else {
|
|
502
|
+
result.reference = 0;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.ws();
|
|
507
|
+
|
|
508
|
+
if (this.gchar('|')) {
|
|
509
|
+
this.ws();
|
|
510
|
+
result.description = this.term().trim();
|
|
511
|
+
this.ws();
|
|
512
|
+
this.fixed('|');
|
|
513
|
+
this.ws();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
result.stop = this.cursor;
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Parse refinements for an expression
|
|
522
|
+
*/
|
|
523
|
+
refinements(expr) {
|
|
524
|
+
let next = true;
|
|
525
|
+
while (next) {
|
|
526
|
+
if (this.peek() !== '{') {
|
|
527
|
+
expr.refinements.push(this.attribute());
|
|
528
|
+
} else {
|
|
529
|
+
expr.refinementGroups.push(this.attributeGroup());
|
|
530
|
+
}
|
|
531
|
+
this.ws();
|
|
532
|
+
next = this.gchar(',');
|
|
533
|
+
this.ws();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Parse an attribute group
|
|
539
|
+
*/
|
|
540
|
+
attributeGroup() {
|
|
541
|
+
const result = new SnomedRefinementGroup();
|
|
542
|
+
|
|
543
|
+
this.fixed('{');
|
|
544
|
+
this.ws();
|
|
545
|
+
result.start = this.cursor;
|
|
546
|
+
|
|
547
|
+
result.refinements.push(this.attribute());
|
|
548
|
+
while (this.gchar(',')) {
|
|
549
|
+
result.refinements.push(this.attribute());
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
result.stop = this.cursor;
|
|
553
|
+
this.ws();
|
|
554
|
+
this.fixed('}');
|
|
555
|
+
this.ws();
|
|
556
|
+
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Parse an attribute (refinement)
|
|
562
|
+
*/
|
|
563
|
+
attribute() {
|
|
564
|
+
const result = new SnomedRefinement();
|
|
565
|
+
result.start = this.cursor;
|
|
566
|
+
|
|
567
|
+
result.name = this.attributeName();
|
|
568
|
+
this.fixed('=');
|
|
569
|
+
result.value = this.attributeValue();
|
|
570
|
+
this.ws();
|
|
571
|
+
|
|
572
|
+
result.stop = this.cursor;
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Parse an attribute name
|
|
578
|
+
*/
|
|
579
|
+
attributeName() {
|
|
580
|
+
const result = new SnomedConcept();
|
|
581
|
+
result.start = this.cursor;
|
|
582
|
+
|
|
583
|
+
this.ws();
|
|
584
|
+
result.code = this.conceptId();
|
|
585
|
+
this.ws();
|
|
586
|
+
|
|
587
|
+
if (this.gchar('|')) {
|
|
588
|
+
this.ws();
|
|
589
|
+
result.description = this.term();
|
|
590
|
+
this.ws();
|
|
591
|
+
this.fixed('|');
|
|
592
|
+
this.ws();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
result.stop = this.cursor;
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Parse an attribute value
|
|
601
|
+
*/
|
|
602
|
+
attributeValue() {
|
|
603
|
+
this.ws();
|
|
604
|
+
|
|
605
|
+
if (this.gchar('(')) {
|
|
606
|
+
const result = this.expression();
|
|
607
|
+
this.fixed(')');
|
|
608
|
+
return result;
|
|
609
|
+
} else {
|
|
610
|
+
return this.expression();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Parse a sub-expression
|
|
616
|
+
*/
|
|
617
|
+
expression() {
|
|
618
|
+
const result = new SnomedExpression();
|
|
619
|
+
result.start = this.cursor;
|
|
620
|
+
|
|
621
|
+
this.ws();
|
|
622
|
+
result.concepts.push(this.concept());
|
|
623
|
+
|
|
624
|
+
while (this.gchar('+')) {
|
|
625
|
+
result.concepts.push(this.concept());
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (this.gchar(':')) {
|
|
629
|
+
this.ws();
|
|
630
|
+
this.refinements(result);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
result.stop = this.cursor;
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Parse a concept ID (sequence of digits)
|
|
639
|
+
*/
|
|
640
|
+
conceptId() {
|
|
641
|
+
let result = '';
|
|
642
|
+
|
|
643
|
+
while (this.isDigit(this.peek())) {
|
|
644
|
+
result += this.next();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.rule(result.length > 0, 'Concept not found (next char = "' + this.peekDisp() + '", in "' + this.source + '")');
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Parse a decimal number
|
|
653
|
+
*/
|
|
654
|
+
decimal() {
|
|
655
|
+
let result = '';
|
|
656
|
+
this.fixed('#');
|
|
657
|
+
|
|
658
|
+
while (this.isDigit(this.peek()) || this.peek() === '.') {
|
|
659
|
+
result += this.next();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Parse a term (text between | characters)
|
|
667
|
+
*/
|
|
668
|
+
term() {
|
|
669
|
+
let result = '';
|
|
670
|
+
|
|
671
|
+
while (this.peek() !== '|') {
|
|
672
|
+
result += this.next();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Parse a string constant
|
|
680
|
+
*/
|
|
681
|
+
stringConstant() {
|
|
682
|
+
let result = '';
|
|
683
|
+
this.fixed('"');
|
|
684
|
+
|
|
685
|
+
while (this.peek() !== '"' && this.peek() !== '\0') {
|
|
686
|
+
if (result.length > MAX_TERM_LENGTH) {
|
|
687
|
+
throw new Error('Constant too long (>' + MAX_TERM_LENGTH + ' chars) at character ' + this.cursor);
|
|
688
|
+
}
|
|
689
|
+
result += this.next();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (this.peek() === '\0') {
|
|
693
|
+
throw new Error('Unterminated Constant at character ' + this.cursor);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.fixed('"');
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Skip whitespace
|
|
702
|
+
*/
|
|
703
|
+
ws() {
|
|
704
|
+
while (this.isWhitespace(this.peek())) {
|
|
705
|
+
this.next();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Try to consume a specific character
|
|
711
|
+
*/
|
|
712
|
+
gchar(c) {
|
|
713
|
+
const result = this.peek() === c;
|
|
714
|
+
if (result) {
|
|
715
|
+
this.next();
|
|
716
|
+
}
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Require a specific character
|
|
722
|
+
*/
|
|
723
|
+
fixed(c) {
|
|
724
|
+
const success = this.gchar(c);
|
|
725
|
+
this.rule(success, 'Expected character "' + c + '" but found ' + this.peek());
|
|
726
|
+
this.ws();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Require a three-character prefix
|
|
731
|
+
*/
|
|
732
|
+
prefix(c) {
|
|
733
|
+
this.fixed(c);
|
|
734
|
+
this.fixed(c);
|
|
735
|
+
this.fixed(c);
|
|
736
|
+
this.ws();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Get the next character and advance cursor
|
|
741
|
+
*/
|
|
742
|
+
next() {
|
|
743
|
+
const result = this.peek();
|
|
744
|
+
this.cursor++;
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Peek at the current character
|
|
750
|
+
*/
|
|
751
|
+
peek() {
|
|
752
|
+
if (this.cursor >= this.source.length) {
|
|
753
|
+
return '\0';
|
|
754
|
+
}
|
|
755
|
+
return this.source[this.cursor];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get a display string for the current character
|
|
760
|
+
*/
|
|
761
|
+
peekDisp() {
|
|
762
|
+
if (this.cursor >= this.source.length) {
|
|
763
|
+
return '[n/a: overrun]';
|
|
764
|
+
}
|
|
765
|
+
return this.source[this.cursor];
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Assert a rule and throw if it fails
|
|
770
|
+
*/
|
|
771
|
+
rule(test, message) {
|
|
772
|
+
if (!test) {
|
|
773
|
+
throw new Error(message + ' at character ' + this.cursor);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Check if character is a digit
|
|
779
|
+
*/
|
|
780
|
+
isDigit(c) {
|
|
781
|
+
return c >= '0' && c <= '9';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Check if character is whitespace
|
|
786
|
+
*/
|
|
787
|
+
isWhitespace(c) {
|
|
788
|
+
return c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Render options for expressions
|
|
793
|
+
const SnomedServicesRenderOption = {
|
|
794
|
+
Minimal: 0,
|
|
795
|
+
AsIs: 1,
|
|
796
|
+
FillMissing: 2,
|
|
797
|
+
ReplaceAll: 3
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// Refinement group match states
|
|
801
|
+
const SnomedRefinementGroupMatchState = {
|
|
802
|
+
NoMatch: 0,
|
|
803
|
+
Identical: 1,
|
|
804
|
+
Subsumed: 2
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Represents a matching concept with optional unmatched refinement groups
|
|
809
|
+
*/
|
|
810
|
+
class MatchingConcept {
|
|
811
|
+
constructor(code, unmatchedGroups = null) {
|
|
812
|
+
this.code = code;
|
|
813
|
+
this.unmatchedGroups = unmatchedGroups || [];
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* SNOMED CT Expression Services
|
|
819
|
+
* Provides comprehensive expression processing capabilities
|
|
820
|
+
*/
|
|
821
|
+
class SnomedExpressionServices {
|
|
822
|
+
constructor(snomedStructures, isAIndex) {
|
|
823
|
+
this.strings = snomedStructures.strings;
|
|
824
|
+
this.words = snomedStructures.words;
|
|
825
|
+
this.stems = snomedStructures.stems;
|
|
826
|
+
this.refs = snomedStructures.refs;
|
|
827
|
+
this.descriptions = snomedStructures.descriptions;
|
|
828
|
+
this.descriptionIndex = snomedStructures.descriptionIndex;
|
|
829
|
+
this.concepts = snomedStructures.concepts;
|
|
830
|
+
this.relationships = snomedStructures.relationships;
|
|
831
|
+
this.refSetMembers = snomedStructures.refSetMembers;
|
|
832
|
+
this.refSetIndex = snomedStructures.refSetIndex;
|
|
833
|
+
|
|
834
|
+
this.isAIndex = isAIndex;
|
|
835
|
+
this.defaultLanguage = 1; // Default to English
|
|
836
|
+
this.building = false; // Set to true during import
|
|
837
|
+
this.assumeClassified = true; // Optimization flag
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Condense an expression to find matching concepts
|
|
842
|
+
*/
|
|
843
|
+
condenseExpression(exp) {
|
|
844
|
+
const grps = [];
|
|
845
|
+
|
|
846
|
+
// Add all refinement groups
|
|
847
|
+
grps.push(...exp.refinementGroups);
|
|
848
|
+
|
|
849
|
+
// Convert individual refinements to single-refinement groups
|
|
850
|
+
for (const ref of exp.refinements) {
|
|
851
|
+
const grp = new SnomedRefinementGroup();
|
|
852
|
+
grp.refinements.push(ref);
|
|
853
|
+
grps.push(grp);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const result = [];
|
|
857
|
+
|
|
858
|
+
if (exp.concepts.length === 1) {
|
|
859
|
+
if (grps.length === 0) {
|
|
860
|
+
result.push(new MatchingConcept(exp.concepts[0].code));
|
|
861
|
+
} else {
|
|
862
|
+
this.findMatchingConcepts(result, exp.concepts[0].reference, grps);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (result.length === 0) {
|
|
867
|
+
throw new Error(`No matches found for ${exp.describe()}`);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Get defining relationships for a concept
|
|
875
|
+
*/
|
|
876
|
+
getDefiningRelationships(conceptIndex) {
|
|
877
|
+
const concept = this.concepts.getConcept(conceptIndex);
|
|
878
|
+
const outboundIndex = concept.outbounds;
|
|
879
|
+
|
|
880
|
+
if (outboundIndex === 0) {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const result = [];
|
|
885
|
+
const outbounds = this.refs.getReferences(outboundIndex);
|
|
886
|
+
|
|
887
|
+
for (const relIndex of outbounds) {
|
|
888
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
889
|
+
|
|
890
|
+
// Only include active defining relationships that are not is-a
|
|
891
|
+
if (rel.active && rel.defining && rel.relType !== this.isAIndex) {
|
|
892
|
+
result.push(relIndex);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Check if groups match exactly
|
|
901
|
+
*/
|
|
902
|
+
groupsMatch(a, b) {
|
|
903
|
+
for (const refA of a.refinements) {
|
|
904
|
+
let refB = null;
|
|
905
|
+
|
|
906
|
+
// Find matching refinement by name
|
|
907
|
+
for (const testRef of b.refinements) {
|
|
908
|
+
if (refA.name.matches(testRef.name)) {
|
|
909
|
+
refB = testRef;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!refB) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Check if values are equivalent
|
|
919
|
+
if (!this.expressionsEquivalent(refA.value, refB.value)) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Parse expression string
|
|
929
|
+
*/
|
|
930
|
+
parseExpression(source) {
|
|
931
|
+
const parser = new SnomedExpressionParser();
|
|
932
|
+
const result = parser.parse(source);
|
|
933
|
+
this.checkExpression(result);
|
|
934
|
+
return result;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Check if two expressions are equivalent
|
|
939
|
+
*/
|
|
940
|
+
expressionsEquivalent(a, b) {
|
|
941
|
+
const e1 = a.canonical();
|
|
942
|
+
const e2 = b.canonical();
|
|
943
|
+
|
|
944
|
+
const match = e1.matches(e2);
|
|
945
|
+
return match === '';
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Create defined expression from concept
|
|
950
|
+
*/
|
|
951
|
+
createDefinedExpression(reference, exp, ancestor = false) {
|
|
952
|
+
if (this.isPrimitive(reference)) {
|
|
953
|
+
if (!exp.hasConcept(reference)) {
|
|
954
|
+
const concept = new SnomedConcept(reference);
|
|
955
|
+
concept.code = this.getConceptId(reference);
|
|
956
|
+
exp.concepts.push(concept);
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
// Add parent concepts
|
|
960
|
+
const parents = this.getConceptParents(reference);
|
|
961
|
+
for (const parent of parents) {
|
|
962
|
+
this.createDefinedExpression(parent, exp, true);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (!ancestor || !this.assumeClassified) {
|
|
966
|
+
const groups = new Map(); // Group number -> refinement group
|
|
967
|
+
|
|
968
|
+
// Process defining relationships
|
|
969
|
+
const definingRels = this.getDefiningRelationships(reference);
|
|
970
|
+
for (const relIndex of definingRels) {
|
|
971
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
972
|
+
|
|
973
|
+
const ref = new SnomedRefinement();
|
|
974
|
+
ref.name = new SnomedConcept(rel.relType);
|
|
975
|
+
ref.name.code = this.getConceptId(rel.relType);
|
|
976
|
+
|
|
977
|
+
ref.value = new SnomedExpression();
|
|
978
|
+
const targetConcept = new SnomedConcept(rel.target);
|
|
979
|
+
targetConcept.code = this.getConceptId(rel.target);
|
|
980
|
+
ref.value.concepts.push(targetConcept);
|
|
981
|
+
|
|
982
|
+
if (rel.group === 0) {
|
|
983
|
+
// Ungrouped refinement
|
|
984
|
+
if (!exp.hasRefinement(ref)) {
|
|
985
|
+
exp.refinements.push(ref);
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
// Grouped refinement
|
|
989
|
+
const groupKey = rel.group.toString();
|
|
990
|
+
if (!groups.has(groupKey)) {
|
|
991
|
+
groups.set(groupKey, new SnomedRefinementGroup());
|
|
992
|
+
}
|
|
993
|
+
groups.get(groupKey).refinements.push(ref);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Add groups to expression
|
|
998
|
+
for (const grp of groups.values()) {
|
|
999
|
+
if (!exp.hasRefinementGroup(grp)) {
|
|
1000
|
+
exp.refinementGroups.push(grp);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Create normal form for a concept
|
|
1009
|
+
*/
|
|
1010
|
+
createNormalForm(reference) {
|
|
1011
|
+
if (this.building) {
|
|
1012
|
+
const exp = new SnomedExpression();
|
|
1013
|
+
this.createDefinedExpression(reference, exp, false);
|
|
1014
|
+
return this.normaliseExpression(exp);
|
|
1015
|
+
} else {
|
|
1016
|
+
// Read from stored normal form
|
|
1017
|
+
const normalFormIndex = this.concepts.getNormalForm(reference);
|
|
1018
|
+
let source;
|
|
1019
|
+
|
|
1020
|
+
if (normalFormIndex === 0) {
|
|
1021
|
+
source = this.getConceptId(reference);
|
|
1022
|
+
} else {
|
|
1023
|
+
source = this.strings.getEntry(normalFormIndex);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const result = new SnomedExpressionParser().parse(source);
|
|
1027
|
+
this.checkExpression(result);
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Rationalize expression by merging concepts and refinements
|
|
1034
|
+
*/
|
|
1035
|
+
rationaliseExpression(exp) {
|
|
1036
|
+
// Merge subsumable concepts
|
|
1037
|
+
let i = 0;
|
|
1038
|
+
while (i < exp.concepts.length) {
|
|
1039
|
+
const c1 = exp.concepts[i];
|
|
1040
|
+
let j = i + 1;
|
|
1041
|
+
|
|
1042
|
+
while (j < exp.concepts.length) {
|
|
1043
|
+
const c2 = exp.concepts[j];
|
|
1044
|
+
|
|
1045
|
+
if (c1.reference !== NO_REFERENCE && c2.reference !== NO_REFERENCE) {
|
|
1046
|
+
if (this.subsumes(c1.reference, c2.reference)) {
|
|
1047
|
+
c1.copyFrom(c2);
|
|
1048
|
+
exp.concepts.splice(j, 1);
|
|
1049
|
+
} else if (this.subsumes(c2.reference, c1.reference)) {
|
|
1050
|
+
exp.concepts.splice(j, 1);
|
|
1051
|
+
} else {
|
|
1052
|
+
j++;
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
j++;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
i++;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Merge refinements
|
|
1062
|
+
this.mergeRefinements(exp.refinements);
|
|
1063
|
+
for (const group of exp.refinementGroups) {
|
|
1064
|
+
this.mergeRefinements(group.refinements);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Merge refinement groups
|
|
1068
|
+
i = 0;
|
|
1069
|
+
while (i < exp.refinementGroups.length) {
|
|
1070
|
+
const grp1 = exp.refinementGroups[i];
|
|
1071
|
+
let j = i + 1;
|
|
1072
|
+
|
|
1073
|
+
while (j < exp.refinementGroups.length) {
|
|
1074
|
+
const grp2 = exp.refinementGroups[j];
|
|
1075
|
+
|
|
1076
|
+
if (this.mergeGroups(grp1, grp2)) {
|
|
1077
|
+
exp.refinementGroups.splice(j, 1);
|
|
1078
|
+
} else {
|
|
1079
|
+
j++;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
i++;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Merge refinement groups if possible
|
|
1088
|
+
*/
|
|
1089
|
+
mergeGroups(grp1, grp2) {
|
|
1090
|
+
// Find matching attribute names
|
|
1091
|
+
const matches = [];
|
|
1092
|
+
const targets = [];
|
|
1093
|
+
|
|
1094
|
+
for (const ref1 of grp1.refinements) {
|
|
1095
|
+
for (const ref2 of grp2.refinements) {
|
|
1096
|
+
if (ref1.name.reference === ref2.name.reference) {
|
|
1097
|
+
matches.push(ref1.name.reference);
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (matches.length === 0) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Check subsumption for each match
|
|
1108
|
+
let canMerge = true;
|
|
1109
|
+
for (const nameRef of matches) {
|
|
1110
|
+
const ref1 = this.getRefinementByName(nameRef, grp1.refinements);
|
|
1111
|
+
const ref2 = this.getRefinementByName(nameRef, grp2.refinements);
|
|
1112
|
+
|
|
1113
|
+
if (!ref1 || !ref2) {
|
|
1114
|
+
canMerge = false;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (this.expressionSubsumes(ref1.value, ref2.value)) {
|
|
1119
|
+
targets.push(true);
|
|
1120
|
+
} else if (this.expressionSubsumes(ref2.value, ref1.value)) {
|
|
1121
|
+
targets.push(false);
|
|
1122
|
+
} else {
|
|
1123
|
+
canMerge = false;
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (canMerge) {
|
|
1129
|
+
// Perform the merge
|
|
1130
|
+
for (let i = 0; i < matches.length; i++) {
|
|
1131
|
+
const nameRef = matches[i];
|
|
1132
|
+
if (targets[i]) {
|
|
1133
|
+
const ref1 = this.getRefinementByName(nameRef, grp1.refinements);
|
|
1134
|
+
const ref2 = this.getRefinementByName(nameRef, grp2.refinements);
|
|
1135
|
+
ref1.value = ref2.value;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Add non-matching refinements from grp2 to grp1
|
|
1140
|
+
for (const ref2 of grp2.refinements) {
|
|
1141
|
+
if (!matches.includes(ref2.name.reference)) {
|
|
1142
|
+
grp1.refinements.push(ref2);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return canMerge;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Get refinement by name reference
|
|
1152
|
+
*/
|
|
1153
|
+
getRefinementByName(nameRef, refinements) {
|
|
1154
|
+
for (const ref of refinements) {
|
|
1155
|
+
if (ref.name.reference === nameRef) {
|
|
1156
|
+
return ref;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Merge refinements in a list
|
|
1164
|
+
*/
|
|
1165
|
+
mergeRefinements(list) {
|
|
1166
|
+
let i = 0;
|
|
1167
|
+
while (i < list.length) {
|
|
1168
|
+
const ref1 = list[i];
|
|
1169
|
+
let j = i + 1;
|
|
1170
|
+
|
|
1171
|
+
while (j < list.length) {
|
|
1172
|
+
const ref2 = list[j];
|
|
1173
|
+
|
|
1174
|
+
if (ref1.name.matches(ref2.name)) {
|
|
1175
|
+
if (this.expressionSubsumes(ref1.value, ref2.value)) {
|
|
1176
|
+
ref1.value = ref2.value;
|
|
1177
|
+
list.splice(j, 1);
|
|
1178
|
+
} else if (this.expressionSubsumes(ref2.value, ref1.value)) {
|
|
1179
|
+
list.splice(j, 1);
|
|
1180
|
+
} else {
|
|
1181
|
+
j++;
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
j++;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
i++;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Normalize expression to normal form
|
|
1193
|
+
*/
|
|
1194
|
+
normaliseExpression(exp) {
|
|
1195
|
+
const work = new SnomedExpression();
|
|
1196
|
+
|
|
1197
|
+
// Process concepts
|
|
1198
|
+
for (const concept of exp.concepts) {
|
|
1199
|
+
if (concept.reference === NO_REFERENCE || this.isPrimitive(concept.reference)) {
|
|
1200
|
+
work.concepts.push(concept);
|
|
1201
|
+
} else {
|
|
1202
|
+
const ex = this.createNormalForm(concept.reference);
|
|
1203
|
+
work.merge(ex);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Process refinements
|
|
1208
|
+
for (const refSrc of exp.refinements) {
|
|
1209
|
+
const refDst = new SnomedRefinement();
|
|
1210
|
+
work.refinements.push(refDst);
|
|
1211
|
+
refDst.name = refSrc.name;
|
|
1212
|
+
refDst.value = this.normaliseExpression(refSrc.value);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Process refinement groups
|
|
1216
|
+
for (const grpSrc of exp.refinementGroups) {
|
|
1217
|
+
const grpDst = new SnomedRefinementGroup();
|
|
1218
|
+
work.refinementGroups.push(grpDst);
|
|
1219
|
+
|
|
1220
|
+
for (const refSrc of grpSrc.refinements) {
|
|
1221
|
+
const refDst = new SnomedRefinement();
|
|
1222
|
+
grpDst.refinements.push(refDst);
|
|
1223
|
+
refDst.name = refSrc.name;
|
|
1224
|
+
refDst.value = this.normaliseExpression(refSrc.value);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const work2 = work.canonical();
|
|
1229
|
+
this.rationaliseExpression(work2);
|
|
1230
|
+
return work2.canonical();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Check if expression a subsumes expression b
|
|
1235
|
+
*/
|
|
1236
|
+
expressionSubsumes(a, b) {
|
|
1237
|
+
if (a.isSimple() && b.isSimple()) {
|
|
1238
|
+
return this.subsumes(a.concepts[0].reference, b.concepts[0].reference);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const e1 = this.normaliseExpression(a);
|
|
1242
|
+
const e2 = this.normaliseExpression(b);
|
|
1243
|
+
|
|
1244
|
+
// Check root concepts
|
|
1245
|
+
for (const c of e1.concepts) {
|
|
1246
|
+
let ok = false;
|
|
1247
|
+
for (const ct of e2.concepts) {
|
|
1248
|
+
if (this.subsumesConcept(c, ct)) {
|
|
1249
|
+
ok = true;
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (!ok) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Check refinement groups
|
|
1259
|
+
for (const r of e1.refinementGroups) {
|
|
1260
|
+
const rt = this.findMatchingGroup(r, e2);
|
|
1261
|
+
if (!rt || !this.subsumesGroup(r, rt)) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Check if concept a subsumes concept b
|
|
1271
|
+
*/
|
|
1272
|
+
subsumesConcept(a, b) {
|
|
1273
|
+
if (a.matches(b)) {
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return (a.reference !== NO_REFERENCE) &&
|
|
1278
|
+
(b.reference !== NO_REFERENCE) &&
|
|
1279
|
+
this.subsumes(a.reference, b.reference);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Check if group a subsumes group b
|
|
1284
|
+
*/
|
|
1285
|
+
subsumesGroup(a, b) {
|
|
1286
|
+
for (const refA of a.refinements) {
|
|
1287
|
+
let refB = null;
|
|
1288
|
+
|
|
1289
|
+
for (const testRef of b.refinements) {
|
|
1290
|
+
if (refA.name.matches(testRef.name)) {
|
|
1291
|
+
refB = testRef;
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (!refB) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (!this.expressionSubsumes(refA.value, refB.value)) {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Find matching group in expression
|
|
1310
|
+
*/
|
|
1311
|
+
findMatchingGroup(r, exp) {
|
|
1312
|
+
for (const t of exp.refinementGroups) {
|
|
1313
|
+
let all = true;
|
|
1314
|
+
|
|
1315
|
+
for (const refs of r.refinements) {
|
|
1316
|
+
let match = false;
|
|
1317
|
+
for (const reft of t.refinements) {
|
|
1318
|
+
if (refs.name.matches(reft.name)) {
|
|
1319
|
+
match = true;
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (!match) {
|
|
1324
|
+
all = false;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (all) {
|
|
1330
|
+
return t;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Find concepts matching refinement patterns
|
|
1339
|
+
*/
|
|
1340
|
+
findMatchingConcepts(list, reference, refinements) {
|
|
1341
|
+
const children = this.getConceptChildren(reference);
|
|
1342
|
+
|
|
1343
|
+
for (const child of children) {
|
|
1344
|
+
const conceptId = this.getConceptId(child);
|
|
1345
|
+
const exp = new SnomedExpression();
|
|
1346
|
+
this.createDefinedExpression(child, exp, false);
|
|
1347
|
+
|
|
1348
|
+
// Convert ungrouped refinements to groups
|
|
1349
|
+
for (const ref of exp.refinements) {
|
|
1350
|
+
const grp = new SnomedRefinementGroup();
|
|
1351
|
+
exp.refinementGroups.push(grp);
|
|
1352
|
+
grp.refinements.push(ref);
|
|
1353
|
+
}
|
|
1354
|
+
exp.refinements.length = 0;
|
|
1355
|
+
|
|
1356
|
+
let allMatched = true;
|
|
1357
|
+
let oneUnMatched = false;
|
|
1358
|
+
const matchedGroups = [];
|
|
1359
|
+
|
|
1360
|
+
for (const grp of exp.refinementGroups) {
|
|
1361
|
+
const state = this.checkGroupStateInRefinements(grp, refinements, matchedGroups);
|
|
1362
|
+
if (state === SnomedRefinementGroupMatchState.NoMatch) {
|
|
1363
|
+
oneUnMatched = true;
|
|
1364
|
+
} else if (state !== SnomedRefinementGroupMatchState.Identical) {
|
|
1365
|
+
allMatched = false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (oneUnMatched) {
|
|
1370
|
+
// Neither this nor children will match
|
|
1371
|
+
continue;
|
|
1372
|
+
} else if (allMatched && matchedGroups.length > 0) {
|
|
1373
|
+
// Complete match
|
|
1374
|
+
list.push(new MatchingConcept(conceptId));
|
|
1375
|
+
} else {
|
|
1376
|
+
// Partial match - continue searching
|
|
1377
|
+
const nonMatchingGroups = this.listNonMatchingGroups(refinements, matchedGroups);
|
|
1378
|
+
if (nonMatchingGroups.length < refinements.length) {
|
|
1379
|
+
list.push(new MatchingConcept(conceptId, nonMatchingGroups));
|
|
1380
|
+
}
|
|
1381
|
+
this.findMatchingConcepts(list, child, nonMatchingGroups);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Check group state in refinements
|
|
1388
|
+
*/
|
|
1389
|
+
checkGroupStateInRefinements(grp, refinements, matchedGroups) {
|
|
1390
|
+
for (const g of refinements) {
|
|
1391
|
+
if (this.groupsMatch(grp, g)) {
|
|
1392
|
+
matchedGroups.push(g);
|
|
1393
|
+
return SnomedRefinementGroupMatchState.Identical;
|
|
1394
|
+
} else if (this.subsumesGroup(grp, g)) {
|
|
1395
|
+
return SnomedRefinementGroupMatchState.Subsumed;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return SnomedRefinementGroupMatchState.NoMatch;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* List non-matching groups
|
|
1403
|
+
*/
|
|
1404
|
+
listNonMatchingGroups(target, source) {
|
|
1405
|
+
const result = [];
|
|
1406
|
+
|
|
1407
|
+
for (const g of target) {
|
|
1408
|
+
const r = this.findMatchingGroupInList(g, source);
|
|
1409
|
+
if (!r) {
|
|
1410
|
+
result.push(g);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return result;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Find matching group in list
|
|
1419
|
+
*/
|
|
1420
|
+
findMatchingGroupInList(r, groups) {
|
|
1421
|
+
for (const t of groups) {
|
|
1422
|
+
let all = true;
|
|
1423
|
+
|
|
1424
|
+
for (const refs of r.refinements) {
|
|
1425
|
+
let match = false;
|
|
1426
|
+
for (const reft of t.refinements) {
|
|
1427
|
+
if (refs.name.matches(reft.name)) {
|
|
1428
|
+
match = true;
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (!match) {
|
|
1433
|
+
all = false;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (all) {
|
|
1439
|
+
return t;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Validate expression structure and concept references
|
|
1448
|
+
*/
|
|
1449
|
+
checkExpression(expression) {
|
|
1450
|
+
for (const concept of expression.concepts) {
|
|
1451
|
+
this.checkConcept(concept);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (expression.hasRefinements()) {
|
|
1455
|
+
for (const refinement of expression.refinements) {
|
|
1456
|
+
this.checkRefinement(refinement);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (expression.hasRefinementGroups()) {
|
|
1461
|
+
for (const group of expression.refinementGroups) {
|
|
1462
|
+
for (const refinement of group.refinements) {
|
|
1463
|
+
this.checkRefinement(refinement);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Validate concept reference
|
|
1471
|
+
*/
|
|
1472
|
+
checkConcept(concept) {
|
|
1473
|
+
if (concept.code) {
|
|
1474
|
+
const conceptId = BigInt(concept.code);
|
|
1475
|
+
const result = this.concepts.findConcept(conceptId);
|
|
1476
|
+
|
|
1477
|
+
if (result.found) {
|
|
1478
|
+
concept.reference = result.index;
|
|
1479
|
+
} else if (concept.code !== '111115') { // Special case for some SNOMED extensions
|
|
1480
|
+
throw new Error(`Concept ${concept.code} not found`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Validate description if provided
|
|
1485
|
+
if (concept.reference !== NO_REFERENCE && concept.description) {
|
|
1486
|
+
const displayNames = this.listDisplayNames(concept.reference, 0);
|
|
1487
|
+
const normalizedDescription = this.normalizeText(concept.description);
|
|
1488
|
+
|
|
1489
|
+
let ok = false;
|
|
1490
|
+
|
|
1491
|
+
// Check if matches preferred display (first in list)
|
|
1492
|
+
if (displayNames.length > 0 && displayNames[0].term) {
|
|
1493
|
+
ok = this.normalizeText(displayNames[0].term) === normalizedDescription;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Check all designations if not already matched
|
|
1497
|
+
if (!ok) {
|
|
1498
|
+
for (const designation of displayNames) {
|
|
1499
|
+
if (this.normalizeText(designation.term) === normalizedDescription) {
|
|
1500
|
+
ok = true;
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!ok) {
|
|
1507
|
+
const validTerms = displayNames.map(d => d.term).join('", "');
|
|
1508
|
+
throw new Error(`Term "${concept.description}" doesn't match a defined term at position ${concept.start} (valid terms would be from this list: "${validTerms}")`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* List all display names for a concept
|
|
1515
|
+
* Equivalent to Pascal ListDisplayNames procedure
|
|
1516
|
+
*/
|
|
1517
|
+
listDisplayNames(conceptIndex, languageFilter = 0) {
|
|
1518
|
+
const designations = [];
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
const concept = this.concepts.getConcept(conceptIndex);
|
|
1522
|
+
const descriptionsRef = concept.descriptions;
|
|
1523
|
+
|
|
1524
|
+
if (descriptionsRef === 0) {
|
|
1525
|
+
return designations; // No descriptions available
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
const descriptionIndices = this.refs.getReferences(descriptionsRef);
|
|
1529
|
+
|
|
1530
|
+
for (let i = 0; i < descriptionIndices.length; i++) {
|
|
1531
|
+
const descIndex = descriptionIndices[i];
|
|
1532
|
+
const description = this.descriptions.getDescription(descIndex);
|
|
1533
|
+
|
|
1534
|
+
// Only include active descriptions
|
|
1535
|
+
if (description.active) {
|
|
1536
|
+
// Language filtering (simplified - could be enhanced)
|
|
1537
|
+
if (languageFilter === 0 || description.lang === languageFilter) {
|
|
1538
|
+
const term = this.strings.getEntry(description.iDesc).trim();
|
|
1539
|
+
|
|
1540
|
+
designations.push({
|
|
1541
|
+
isPreferred: i === 0, // First description is considered preferred
|
|
1542
|
+
isActive: true,
|
|
1543
|
+
languageCode: this.codeForLanguage(description.lang),
|
|
1544
|
+
term: term,
|
|
1545
|
+
descriptionIndex: descIndex
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
// If we can't read the concept descriptions, return empty list
|
|
1552
|
+
this.log.warn(`Warning: Could not read descriptions for concept ${conceptIndex}: ${error.message}`);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return designations;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Normalize text for comparison (equivalent to Pascal normalise function)
|
|
1560
|
+
*/
|
|
1561
|
+
normalizeText(text) {
|
|
1562
|
+
if (!text) return '';
|
|
1563
|
+
|
|
1564
|
+
let result = '';
|
|
1565
|
+
let wasWhitespace = false;
|
|
1566
|
+
|
|
1567
|
+
for (const char of text) {
|
|
1568
|
+
if (this.isWhitespace(char)) {
|
|
1569
|
+
if (!wasWhitespace) {
|
|
1570
|
+
result += ' ';
|
|
1571
|
+
wasWhitespace = true;
|
|
1572
|
+
}
|
|
1573
|
+
} else {
|
|
1574
|
+
result += char.toLowerCase();
|
|
1575
|
+
wasWhitespace = false;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return result.trim();
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* Check if character is whitespace
|
|
1584
|
+
*/
|
|
1585
|
+
isWhitespace(char) {
|
|
1586
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Get language code from internal language index
|
|
1591
|
+
*/
|
|
1592
|
+
codeForLanguage(langIndex) {
|
|
1593
|
+
// Simplified mapping - could be enhanced with proper language reference
|
|
1594
|
+
const languageMap = {
|
|
1595
|
+
1: 'en-US',
|
|
1596
|
+
2: 'en-GB',
|
|
1597
|
+
3: 'es',
|
|
1598
|
+
4: 'fr',
|
|
1599
|
+
5: 'de'
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
return languageMap[langIndex] || 'en-US';
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Validate refinement
|
|
1607
|
+
*/
|
|
1608
|
+
checkRefinement(refinement) {
|
|
1609
|
+
this.checkConcept(refinement.name);
|
|
1610
|
+
this.checkExpression(refinement.value);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Render expression as string
|
|
1615
|
+
*/
|
|
1616
|
+
renderExpression(source, option = SnomedServicesRenderOption.AsIs) {
|
|
1617
|
+
const parts = [];
|
|
1618
|
+
this.renderExpressionParts(parts, source, option);
|
|
1619
|
+
return parts.join('');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Helper methods that need to be implemented based on SNOMED structures
|
|
1624
|
+
*/
|
|
1625
|
+
|
|
1626
|
+
isPrimitive(reference) {
|
|
1627
|
+
// Check if concept is primitive based on concept flags
|
|
1628
|
+
// In SNOMED CT, primitive concepts are not fully defined by their relationships
|
|
1629
|
+
try {
|
|
1630
|
+
const concept = this.concepts.getConcept(reference);
|
|
1631
|
+
// Bit 0 typically indicates if concept is primitive (1) or defined (0)
|
|
1632
|
+
// This may vary based on the specific implementation
|
|
1633
|
+
return (concept.flags & 1) !== 0;
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
// If we can't read the concept, assume it's primitive for safety
|
|
1636
|
+
this.log.warn(`Warning: Could not check primitive status for concept ${reference}: ${error.message}`);
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
getConceptId(reference) {
|
|
1642
|
+
// Get concept ID string from reference index
|
|
1643
|
+
try {
|
|
1644
|
+
const concept = this.concepts.getConcept(reference);
|
|
1645
|
+
return concept.identity.toString();
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
this.log.warn(`Warning: Could not get concept ID for reference ${reference}: ${error.message}`);
|
|
1648
|
+
return reference.toString(); // Fall back to using the reference itself
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
getConceptParents(reference) {
|
|
1653
|
+
// Get active parent concepts (needs implementation)
|
|
1654
|
+
const concept = this.concepts.getConcept(reference);
|
|
1655
|
+
const parentsIndex = concept.parents;
|
|
1656
|
+
|
|
1657
|
+
if (parentsIndex === 0) {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return this.refs.getReferences(parentsIndex);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
getConceptChildren(reference) {
|
|
1665
|
+
// Get child concepts (needs implementation based on inbound is-a relationships)
|
|
1666
|
+
const children = [];
|
|
1667
|
+
const concept = this.concepts.getConcept(reference);
|
|
1668
|
+
const inboundsIndex = concept.inbounds;
|
|
1669
|
+
|
|
1670
|
+
if (inboundsIndex === 0) {
|
|
1671
|
+
return children;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const inbounds = this.refs.getReferences(inboundsIndex);
|
|
1675
|
+
|
|
1676
|
+
for (const relIndex of inbounds) {
|
|
1677
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
1678
|
+
|
|
1679
|
+
if (rel.active && rel.defining && rel.relType === this.isAIndex) {
|
|
1680
|
+
children.push(rel.source);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
return children;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
subsumes(a, b) {
|
|
1688
|
+
// Check if concept a subsumes concept b using closure/descendants
|
|
1689
|
+
if (a === b) {
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
try {
|
|
1694
|
+
// Get the closure (all descendants) for concept a
|
|
1695
|
+
// ?? const conceptA = this.concepts.getConcept(a);
|
|
1696
|
+
const closureRef = this.concepts.getAllDesc(a);
|
|
1697
|
+
|
|
1698
|
+
if (closureRef === 0 || closureRef === 0xFFFFFFFF) {
|
|
1699
|
+
// No closure data or magic "no children" value
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Get the array of descendant indices
|
|
1704
|
+
const descendants = this.refs.getReferences(closureRef);
|
|
1705
|
+
|
|
1706
|
+
// Check if b is in the descendants of a
|
|
1707
|
+
return descendants.includes(b);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
// If we can't read closure data, fall back to simple equality check
|
|
1710
|
+
this.log.warn(`Warning: Could not check subsumption for ${a} -> ${b}: ${error.message}`);
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Helper to render expression parts
|
|
1717
|
+
*/
|
|
1718
|
+
renderExpressionParts(parts, expr, option) {
|
|
1719
|
+
// Render concepts
|
|
1720
|
+
for (let i = 0; i < expr.concepts.length; i++) {
|
|
1721
|
+
if (i > 0) parts.push('+');
|
|
1722
|
+
this.renderConcept(parts, expr.concepts[i], option);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Render refinements and groups
|
|
1726
|
+
if (expr.hasRefinements() || expr.hasRefinementGroups()) {
|
|
1727
|
+
parts.push(':');
|
|
1728
|
+
|
|
1729
|
+
// Ungrouped refinements
|
|
1730
|
+
if (expr.hasRefinements()) {
|
|
1731
|
+
for (let i = 0; i < expr.refinements.length; i++) {
|
|
1732
|
+
if (i > 0) parts.push(',');
|
|
1733
|
+
this.renderRefinement(parts, expr.refinements[i], option);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Grouped refinements
|
|
1738
|
+
if (expr.hasRefinementGroups()) {
|
|
1739
|
+
for (let j = 0; j < expr.refinementGroups.length; j++) {
|
|
1740
|
+
if (j > 0) parts.push(',');
|
|
1741
|
+
parts.push('{');
|
|
1742
|
+
|
|
1743
|
+
for (let i = 0; i < expr.refinementGroups[j].refinements.length; i++) {
|
|
1744
|
+
if (i > 0) parts.push(',');
|
|
1745
|
+
this.renderRefinement(parts, expr.refinementGroups[j].refinements[i], option);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
parts.push('}');
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
renderConcept(parts, expr, option) {
|
|
1755
|
+
if (expr.reference !== NO_REFERENCE && expr.code === '') {
|
|
1756
|
+
expr.code = this.getConceptId(expr.reference);
|
|
1757
|
+
parts.push(expr.code);
|
|
1758
|
+
} else {
|
|
1759
|
+
parts.push(expr.describe());
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
let description = '';
|
|
1763
|
+
switch (option) {
|
|
1764
|
+
case SnomedServicesRenderOption.Minimal:
|
|
1765
|
+
description = '';
|
|
1766
|
+
break;
|
|
1767
|
+
case SnomedServicesRenderOption.AsIs:
|
|
1768
|
+
description = expr.description;
|
|
1769
|
+
break;
|
|
1770
|
+
case SnomedServicesRenderOption.FillMissing:
|
|
1771
|
+
description = expr.description;
|
|
1772
|
+
if (description === '') {
|
|
1773
|
+
if (expr.reference !== NO_REFERENCE) {
|
|
1774
|
+
description = this.getDisplayName(expr.reference);
|
|
1775
|
+
} else if (expr.code) {
|
|
1776
|
+
description = this.getDisplayName(expr.code);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
break;
|
|
1780
|
+
case SnomedServicesRenderOption.ReplaceAll:
|
|
1781
|
+
if (expr.code) {
|
|
1782
|
+
description = this.getDisplayName(expr.code);
|
|
1783
|
+
}
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (description) {
|
|
1788
|
+
parts.push('|');
|
|
1789
|
+
parts.push(description);
|
|
1790
|
+
parts.push('|');
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
renderRefinement(parts, expr, option) {
|
|
1795
|
+
this.renderConcept(parts, expr.name, option);
|
|
1796
|
+
parts.push('=');
|
|
1797
|
+
this.renderExpressionParts(parts, expr.value, option);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
getDisplayName(conceptIdOrReference) {
|
|
1801
|
+
// Get display name for concept
|
|
1802
|
+
let conceptIndex;
|
|
1803
|
+
|
|
1804
|
+
if (typeof conceptIdOrReference === 'string') {
|
|
1805
|
+
// It's a concept ID string, need to find the concept
|
|
1806
|
+
const conceptId = BigInt(conceptIdOrReference);
|
|
1807
|
+
const result = this.concepts.findConcept(conceptId);
|
|
1808
|
+
if (!result.found) {
|
|
1809
|
+
return '';
|
|
1810
|
+
}
|
|
1811
|
+
conceptIndex = result.index;
|
|
1812
|
+
} else {
|
|
1813
|
+
// It's already a concept index
|
|
1814
|
+
conceptIndex = conceptIdOrReference;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Get the preferred display name (FSN or first active description)
|
|
1818
|
+
const displayNames = this.listDisplayNames(conceptIndex, this.defaultLanguage);
|
|
1819
|
+
|
|
1820
|
+
if (displayNames.length > 0) {
|
|
1821
|
+
// Look for FSN first (if we can detect it), otherwise use first available
|
|
1822
|
+
for (const designation of displayNames) {
|
|
1823
|
+
if (designation.isPreferred) {
|
|
1824
|
+
return designation.term;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// Fall back to first designation
|
|
1828
|
+
return displayNames[0].term;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return '';
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Expression context for maintaining state during processing
|
|
1837
|
+
*/
|
|
1838
|
+
class SnomedExpressionContext {
|
|
1839
|
+
constructor(source, expression) {
|
|
1840
|
+
this.source = source || '';
|
|
1841
|
+
this.expression = expression;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
static fromReference(reference) {
|
|
1845
|
+
const expression = new SnomedExpression();
|
|
1846
|
+
expression.concepts.push(new SnomedConcept(reference));
|
|
1847
|
+
return new SnomedExpressionContext('', expression);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
static fromSource(source, reference) {
|
|
1851
|
+
const expression = new SnomedExpression();
|
|
1852
|
+
expression.concepts.push(new SnomedConcept(reference));
|
|
1853
|
+
return new SnomedExpressionContext(source, expression);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
isComplex() {
|
|
1857
|
+
return this.expression.isComplex();
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
getReference() {
|
|
1861
|
+
return this.expression.concepts[0].reference;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
setExpression(expression) {
|
|
1865
|
+
this.expression = expression;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
module.exports = {
|
|
1870
|
+
SnomedExpressionServices,
|
|
1871
|
+
SnomedExpressionContext,
|
|
1872
|
+
MatchingConcept,
|
|
1873
|
+
SnomedServicesRenderOption,
|
|
1874
|
+
SnomedRefinementGroupMatchState
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// Also add this method to SnomedExpressionServices to help with debugging
|
|
1878
|
+
class SnomedExpressionServicesExtended extends SnomedExpressionServices {
|
|
1879
|
+
constructor(snomedStructures, isAIndex) {
|
|
1880
|
+
super(snomedStructures, isAIndex);
|
|
1881
|
+
|
|
1882
|
+
// Ensure building flag is accessible
|
|
1883
|
+
this.building = false;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Enhanced getDefiningRelationships with better error handling
|
|
1888
|
+
*/
|
|
1889
|
+
getDefiningRelationships(conceptIndex) {
|
|
1890
|
+
try {
|
|
1891
|
+
const concept = this.concepts.getConcept(conceptIndex);
|
|
1892
|
+
const outboundIndex = concept.outbounds;
|
|
1893
|
+
|
|
1894
|
+
if (outboundIndex === 0) {
|
|
1895
|
+
return [];
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const result = [];
|
|
1899
|
+
const outbounds = this.refs.getReferences(outboundIndex);
|
|
1900
|
+
|
|
1901
|
+
if (!outbounds) {
|
|
1902
|
+
return [];
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
for (const relIndex of outbounds) {
|
|
1906
|
+
try {
|
|
1907
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
1908
|
+
|
|
1909
|
+
// Only include active defining relationships that are not is-a
|
|
1910
|
+
if (rel.active && rel.defining && rel.relType !== this.isAIndex) {
|
|
1911
|
+
result.push(relIndex);
|
|
1912
|
+
}
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
// Skip problematic relationships
|
|
1915
|
+
if (this.building) {
|
|
1916
|
+
this.log.warn(`Warning: Could not read relationship ${relIndex}: ${error.message}`);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
return result;
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
if (this.building) {
|
|
1924
|
+
this.log.warn(`Warning: Could not get defining relationships for concept ${conceptIndex}: ${error.message}`);
|
|
1925
|
+
}
|
|
1926
|
+
return [];
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* Enhanced getConceptParents with better error handling
|
|
1932
|
+
*/
|
|
1933
|
+
getConceptParents(reference) {
|
|
1934
|
+
try {
|
|
1935
|
+
const concept = this.concepts.getConcept(reference);
|
|
1936
|
+
const parentsIndex = concept.parents;
|
|
1937
|
+
|
|
1938
|
+
if (parentsIndex === 0) {
|
|
1939
|
+
return [];
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
const parents = this.refs.getReferences(parentsIndex);
|
|
1943
|
+
return parents || [];
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (this.building) {
|
|
1946
|
+
this.log.warn(`Warning: Could not get parents for concept ${reference}: ${error.message}`);
|
|
1947
|
+
}
|
|
1948
|
+
return [];
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Enhanced isPrimitive check with fallback
|
|
1954
|
+
*/
|
|
1955
|
+
isPrimitive(reference) {
|
|
1956
|
+
try {
|
|
1957
|
+
const concept = this.concepts.getConcept(reference);
|
|
1958
|
+
// In SNOMED CT, primitive concepts have the primitive flag set
|
|
1959
|
+
// The definitionStatusId in RF2 determines this:
|
|
1960
|
+
// 900000000000074008 = primitive, 900000000000073002 = fully defined
|
|
1961
|
+
return (concept.flags & 1) !== 0;
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
// If we can't read the concept, assume it's primitive for safety
|
|
1964
|
+
if (this.building) {
|
|
1965
|
+
this.log.warn(`Warning: Could not check primitive status for concept ${reference}, assuming primitive: ${error.message}`);
|
|
1966
|
+
}
|
|
1967
|
+
return true;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Enhanced createDefinedExpression with better error handling
|
|
1973
|
+
*/
|
|
1974
|
+
createDefinedExpression(reference, exp, ancestor = false) {
|
|
1975
|
+
try {
|
|
1976
|
+
if (this.isPrimitive(reference)) {
|
|
1977
|
+
if (!exp.hasConcept(reference)) {
|
|
1978
|
+
const concept = new SnomedConcept(reference);
|
|
1979
|
+
concept.code = this.getConceptId(reference);
|
|
1980
|
+
exp.concepts.push(concept);
|
|
1981
|
+
}
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Add parent concepts
|
|
1986
|
+
const parents = this.getConceptParents(reference);
|
|
1987
|
+
for (const parent of parents) {
|
|
1988
|
+
this.createDefinedExpression(parent, exp, true);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (!ancestor || !this.assumeClassified) {
|
|
1992
|
+
const groups = new Map(); // Group number -> refinement group
|
|
1993
|
+
|
|
1994
|
+
// Process defining relationships
|
|
1995
|
+
const definingRels = this.getDefiningRelationships(reference);
|
|
1996
|
+
for (const relIndex of definingRels) {
|
|
1997
|
+
try {
|
|
1998
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
1999
|
+
|
|
2000
|
+
const ref = new SnomedRefinement();
|
|
2001
|
+
ref.name = new SnomedConcept(rel.relType);
|
|
2002
|
+
ref.name.code = this.getConceptId(rel.relType);
|
|
2003
|
+
|
|
2004
|
+
ref.value = new SnomedExpression();
|
|
2005
|
+
const targetConcept = new SnomedConcept(rel.target);
|
|
2006
|
+
targetConcept.code = this.getConceptId(rel.target);
|
|
2007
|
+
ref.value.concepts.push(targetConcept);
|
|
2008
|
+
|
|
2009
|
+
if (rel.group === 0) {
|
|
2010
|
+
// Ungrouped refinement
|
|
2011
|
+
if (!exp.hasRefinement(ref)) {
|
|
2012
|
+
exp.refinements.push(ref);
|
|
2013
|
+
}
|
|
2014
|
+
} else {
|
|
2015
|
+
// Grouped refinement
|
|
2016
|
+
const groupKey = rel.group.toString();
|
|
2017
|
+
if (!groups.has(groupKey)) {
|
|
2018
|
+
groups.set(groupKey, new SnomedRefinementGroup());
|
|
2019
|
+
}
|
|
2020
|
+
groups.get(groupKey).refinements.push(ref);
|
|
2021
|
+
}
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
// Skip problematic relationships but continue
|
|
2024
|
+
if (this.building) {
|
|
2025
|
+
this.log.warn(`Warning: Could not process relationship ${relIndex}: ${error.message}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Add groups to expression
|
|
2031
|
+
for (const grp of groups.values()) {
|
|
2032
|
+
if (!exp.hasRefinementGroup(grp)) {
|
|
2033
|
+
exp.refinementGroups.push(grp);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
if (this.building) {
|
|
2039
|
+
this.log.warn(`Warning: Could not create defined expression for concept ${reference}: ${error.message}`);
|
|
2040
|
+
}
|
|
2041
|
+
// Add as primitive concept as fallback
|
|
2042
|
+
if (!exp.hasConcept(reference)) {
|
|
2043
|
+
const concept = new SnomedConcept(reference);
|
|
2044
|
+
concept.code = this.getConceptId(reference);
|
|
2045
|
+
exp.concepts.push(concept);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
// Export classes and constants
|
|
2053
|
+
module.exports = {
|
|
2054
|
+
// Expression status and constants
|
|
2055
|
+
SnomedExpressionStatus,
|
|
2056
|
+
MAX_TERM_LENGTH,
|
|
2057
|
+
NO_REFERENCE,
|
|
2058
|
+
|
|
2059
|
+
// Core expression classes
|
|
2060
|
+
SnomedExpressionBase,
|
|
2061
|
+
SnomedConcept,
|
|
2062
|
+
SnomedRefinement,
|
|
2063
|
+
SnomedRefinementGroup,
|
|
2064
|
+
SnomedExpression,
|
|
2065
|
+
SnomedExpressionParser,
|
|
2066
|
+
|
|
2067
|
+
// Services and context
|
|
2068
|
+
SnomedExpressionServices,
|
|
2069
|
+
SnomedExpressionContext,
|
|
2070
|
+
MatchingConcept,
|
|
2071
|
+
SnomedExpressionServicesExtended,
|
|
2072
|
+
|
|
2073
|
+
// Enums and constants
|
|
2074
|
+
SnomedServicesRenderOption,
|
|
2075
|
+
SnomedRefinementGroupMatchState
|
|
2076
|
+
};
|
|
2077
|
+
|