@cosyte/terminology 0.1.5 → 0.1.7

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.mjs CHANGED
@@ -210,7 +210,7 @@ var FATAL_CODES = {
210
210
  /**
211
211
  * An **inversion** of a directional crosswalk was requested: a forbidden operation. The GEMs and
212
212
  * the SNOMED→ICD-10-CM map are authoritative in **one direction only**; a forward map is not the
213
- * inverse of the backward map. Thrown by {@link ../crosswalk/gems.invertGem} so the
213
+ * inverse of the backward map. Thrown by {@link invertGem} so the
214
214
  * never-invert refusal is a first-class, discoverable contract rather than a silent absence.
215
215
  */
216
216
  TERM_MAP_NOT_INVERTIBLE: "TERM_MAP_NOT_INVERTIBLE"
@@ -444,6 +444,10 @@ function loadUnmapped(raw, path) {
444
444
  assignString(out, raw, "url");
445
445
  return Object.freeze(out);
446
446
  }
447
+ var loadedGroups = /* @__PURE__ */ new WeakSet();
448
+ function isLoadedGroup(group) {
449
+ return loadedGroups.has(group);
450
+ }
447
451
  function loadGroup(raw, path) {
448
452
  if (!isJsonObject(raw)) malformed(path, "group is not an object");
449
453
  const rawElements = getArray(raw, "element") ?? [];
@@ -458,7 +462,9 @@ function loadGroup(raw, path) {
458
462
  if (isJsonObject(raw) && raw["unmapped"] !== void 0) {
459
463
  out.unmapped = loadUnmapped(raw["unmapped"], `${path}.unmapped`);
460
464
  }
461
- return Object.freeze(out);
465
+ const group = Object.freeze(out);
466
+ loadedGroups.add(group);
467
+ return group;
462
468
  }
463
469
  function loadConceptMap(json) {
464
470
  if (!isJsonObject(json)) malformed("", "resource is not a JSON object");
@@ -502,6 +508,24 @@ function groupApplies(group, system) {
502
508
  if (system === void 0) return true;
503
509
  return group.source === void 0 || group.source === system;
504
510
  }
511
+ var elementIndex = /* @__PURE__ */ new WeakMap();
512
+ var NO_ELEMENTS = Object.freeze([]);
513
+ function candidates(group, code) {
514
+ if (typeof code !== "string" || !isLoadedGroup(group)) return group.element;
515
+ let index = elementIndex.get(group);
516
+ if (index === void 0) {
517
+ const built = /* @__PURE__ */ new Map();
518
+ for (const element of group.element) {
519
+ if (element.code === void 0) continue;
520
+ const list = built.get(element.code);
521
+ if (list === void 0) built.set(element.code, [element]);
522
+ else list.push(element);
523
+ }
524
+ index = built;
525
+ elementIndex.set(group, index);
526
+ }
527
+ return index.get(code) ?? NO_ELEMENTS;
528
+ }
505
529
  function provenanceOf(map, group, sourceSystem) {
506
530
  const out = {};
507
531
  if (map.url !== void 0) out.conceptMapUrl = map.url;
@@ -535,7 +559,7 @@ function translate(sourceCoding, map) {
535
559
  for (const group of map.group) {
536
560
  if (!groupApplies(group, system)) continue;
537
561
  if (fallbackGroup === void 0 && group.unmapped !== void 0) fallbackGroup = group;
538
- for (const element of group.element) {
562
+ for (const element of candidates(group, source.code)) {
539
563
  if (element.code !== source.code) continue;
540
564
  sourceCodePresent = true;
541
565
  for (const t of element.target) {
@@ -638,6 +662,12 @@ function frozenMap(source, what) {
638
662
  return Object.freeze(view);
639
663
  }
640
664
 
665
+ // src/common/own-string.ts
666
+ var VIEW_MIN_LENGTH = 13;
667
+ function ownString(s) {
668
+ return s.length < VIEW_MIN_LENGTH ? s : ` ${s}`.slice(1);
669
+ }
670
+
641
671
  // src/codesystem/status.ts
642
672
  var defaultStatusMapper = (raw) => {
643
673
  switch (raw.trim().toUpperCase()) {
@@ -672,7 +702,79 @@ function makeStatus(activity, raw, billable) {
672
702
  }
673
703
 
674
704
  // src/codesystem/csv.ts
675
- function parseCsvRows(content) {
705
+ var QUOTE = 34;
706
+ var COMMA = 44;
707
+ var LINE_FEED = 10;
708
+ var CARRIAGE_RETURN = 13;
709
+ function scanCsv(content, sink) {
710
+ if (typeof content !== "string") return readCsvByCharAt(content, sink);
711
+ const n = content.length;
712
+ const cells = [];
713
+ let column = 0;
714
+ const keeps = (col) => sink.wanted === void 0 || sink.wanted[col] === true;
715
+ let keep = keeps(0);
716
+ let field = "";
717
+ let runStart = 0;
718
+ let inQuotes = false;
719
+ let fieldStart = true;
720
+ let sawAny = false;
721
+ const endField = (end) => {
722
+ if (keep) cells[column] = field + content.slice(runStart, end);
723
+ field = "";
724
+ };
725
+ const endRow = () => {
726
+ sink.row(cells, column + 1);
727
+ column = 0;
728
+ keep = keeps(0);
729
+ };
730
+ for (let i = 0; i < n; i++) {
731
+ const ch = content.charCodeAt(i);
732
+ if (inQuotes) {
733
+ if (ch === QUOTE) {
734
+ if (content.charCodeAt(i + 1) === QUOTE) {
735
+ if (keep) field += content.slice(runStart, i + 1);
736
+ i++;
737
+ } else {
738
+ if (keep) field += content.slice(runStart, i);
739
+ inQuotes = false;
740
+ }
741
+ runStart = i + 1;
742
+ }
743
+ continue;
744
+ }
745
+ if (ch === QUOTE && fieldStart) {
746
+ inQuotes = true;
747
+ fieldStart = false;
748
+ sawAny = true;
749
+ runStart = i + 1;
750
+ } else if (ch === COMMA) {
751
+ endField(i);
752
+ column++;
753
+ keep = keeps(column);
754
+ runStart = i + 1;
755
+ fieldStart = true;
756
+ sawAny = true;
757
+ } else if (ch === LINE_FEED) {
758
+ endField(i);
759
+ endRow();
760
+ runStart = i + 1;
761
+ fieldStart = true;
762
+ sawAny = false;
763
+ } else if (ch === CARRIAGE_RETURN) {
764
+ if (keep) field += content.slice(runStart, i);
765
+ runStart = i + 1;
766
+ } else {
767
+ fieldStart = false;
768
+ sawAny = true;
769
+ }
770
+ }
771
+ if (sawAny) {
772
+ endField(n);
773
+ endRow();
774
+ }
775
+ return inQuotes;
776
+ }
777
+ function readCsvByCharAt(content, sink) {
676
778
  const rows = [];
677
779
  let row = [];
678
780
  let field = "";
@@ -711,20 +813,30 @@ function parseCsvRows(content) {
711
813
  field = "";
712
814
  fieldStart = true;
713
815
  sawAny = false;
714
- } else if (ch === "\r") ; else {
816
+ } else if (ch !== "\r") {
715
817
  field += ch;
716
818
  fieldStart = false;
717
819
  sawAny = true;
718
820
  }
719
821
  }
720
- if (sawAny || field !== "" || row.length > 0) {
822
+ if (sawAny) {
721
823
  row.push(field);
722
824
  rows.push(row);
723
825
  }
724
- return { rows, unterminatedQuote: inQuotes };
826
+ for (const r of rows) sink.row(r, r.length);
827
+ return inQuotes;
725
828
  }
726
829
  function parseCsv(content) {
727
- return parseCsvRows(content).rows;
830
+ const rows = [];
831
+ scanCsv(content, {
832
+ wanted: void 0,
833
+ row: (cells, count) => {
834
+ const row = [];
835
+ for (let c = 0; c < count; c++) row.push(ownString(cells[c]));
836
+ rows.push(row);
837
+ }
838
+ });
839
+ return rows;
728
840
  }
729
841
  var MISSING_COLUMN_MESSAGE = Object.freeze({
730
842
  code: "CSV source: the configured 'code' column is not present in the header row",
@@ -741,47 +853,44 @@ function requireHeader(header, name, role) {
741
853
  }
742
854
  function parseCsvSource(source) {
743
855
  const statusMap = source.statusMap ?? defaultStatusMapper;
744
- const { rows, unterminatedQuote } = parseCsvRows(source.content);
745
- const header = rows[0];
746
- if (header === void 0) {
747
- throw new TerminologyError(
748
- FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
749
- "CSV source: the file has no header row"
750
- );
751
- }
752
- const warnings = [];
753
- if (unterminatedQuote) {
754
- warnings.push(
755
- Object.freeze({
756
- code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
757
- line: rows.length,
758
- detail: "an opened quoted field was not terminated before end-of-input"
759
- })
760
- );
761
- }
762
- const codeIdx = requireHeader(header, source.columns.code, "code");
763
- const displayIdx = requireHeader(header, source.columns.display, "display");
764
- const statusIdx = source.columns.status !== void 0 ? requireHeader(header, source.columns.status, "status") : void 0;
765
- const propIdx = (source.columns.properties ?? []).map((n) => ({
766
- name: n,
767
- index: requireHeader(header, n, "property")
768
- }));
769
- const needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
856
+ const content = source.content;
770
857
  const concepts = /* @__PURE__ */ new Map();
771
- for (let r = 1; r < rows.length; r++) {
772
- const cells = rows[r] ?? [];
773
- const lineNo = r + 1;
774
- if (cells.length < needCols) {
858
+ const warnings = [];
859
+ let rowCount = 0;
860
+ let codeIdx = 0;
861
+ let displayIdx = 0;
862
+ let statusIdx;
863
+ const propIdx = [];
864
+ let needCols = 0;
865
+ const header = (cells, count) => {
866
+ const names = cells.slice(0, count);
867
+ codeIdx = requireHeader(names, source.columns.code, "code");
868
+ displayIdx = requireHeader(names, source.columns.display, "display");
869
+ statusIdx = source.columns.status !== void 0 ? requireHeader(names, source.columns.status, "status") : void 0;
870
+ for (const n of source.columns.properties ?? []) {
871
+ propIdx.push({ name: n, index: requireHeader(names, n, "property") });
872
+ }
873
+ needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
874
+ const wanted = [];
875
+ wanted[codeIdx] = true;
876
+ wanted[displayIdx] = true;
877
+ if (statusIdx !== void 0) wanted[statusIdx] = true;
878
+ for (const p of propIdx) wanted[p.index] = true;
879
+ sink.wanted = wanted;
880
+ };
881
+ const dataRow = (cells, count, lineNo) => {
882
+ if (count < needCols) {
775
883
  warnings.push(
776
884
  Object.freeze({
777
885
  code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
778
886
  line: lineNo,
779
- detail: `row has ${String(cells.length)} fields, need ${String(needCols)}`
887
+ detail: `row has ${String(count)} fields, need ${String(needCols)}`
780
888
  })
781
889
  );
782
- continue;
890
+ return;
783
891
  }
784
- const code = (cells[codeIdx] ?? "").trim();
892
+ const cell3 = (index) => cells[index];
893
+ const code = cell3(codeIdx).trim();
785
894
  if (code === "") {
786
895
  warnings.push(
787
896
  Object.freeze({
@@ -790,23 +899,47 @@ function parseCsvSource(source) {
790
899
  detail: "row is missing its code"
791
900
  })
792
901
  );
793
- continue;
902
+ return;
794
903
  }
795
- if (concepts.has(code)) continue;
796
- const out = { code, properties: [] };
797
- const display = (cells[displayIdx] ?? "").trim();
798
- if (display !== "") out.display = display;
904
+ if (concepts.has(code)) return;
905
+ const out = { code: ownString(code), properties: [] };
906
+ const display = cell3(displayIdx).trim();
907
+ if (display !== "") out.display = ownString(display);
799
908
  if (statusIdx !== void 0) {
800
- const rawStatus = (cells[statusIdx] ?? "").trim();
909
+ const rawStatus = ownString(cell3(statusIdx).trim());
801
910
  out.status = makeStatus(statusMap(rawStatus), rawStatus);
802
911
  }
803
912
  const props = [];
804
913
  for (const p of propIdx) {
805
- const value = (cells[p.index] ?? "").trim();
806
- if (value !== "") props.push(Object.freeze({ code: p.name, value }));
914
+ const value = cell3(p.index).trim();
915
+ if (value !== "") props.push(Object.freeze({ code: p.name, value: ownString(value) }));
807
916
  }
808
917
  out.properties = Object.freeze(props);
809
- concepts.set(code, Object.freeze(out));
918
+ concepts.set(out.code, Object.freeze(out));
919
+ };
920
+ const sink = {
921
+ wanted: void 0,
922
+ row: (cells, count) => {
923
+ rowCount++;
924
+ if (rowCount === 1) header(cells, count);
925
+ else dataRow(cells, count, rowCount);
926
+ }
927
+ };
928
+ const unterminatedQuote = scanCsv(content, sink);
929
+ if (rowCount === 0) {
930
+ throw new TerminologyError(
931
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
932
+ "CSV source: the file has no header row"
933
+ );
934
+ }
935
+ if (unterminatedQuote) {
936
+ warnings.unshift(
937
+ Object.freeze({
938
+ code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
939
+ line: rowCount,
940
+ detail: "an opened quoted field was not terminated before end-of-input"
941
+ })
942
+ );
810
943
  }
811
944
  return { concepts, warnings };
812
945
  }
@@ -854,9 +987,10 @@ function collectConcepts(rawConcepts, into, warnings, parentCode) {
854
987
  if (code !== void 0 && code !== "" && !into.has(code)) {
855
988
  const props = [];
856
989
  for (const p of getArray(raw, "property") ?? []) {
990
+ if (!isJsonObject(p)) continue;
857
991
  const pcode = getString(p, "code");
858
992
  if (pcode === void 0) continue;
859
- const value = isJsonObject(p) ? readPropertyValue(p) : void 0;
993
+ const value = readPropertyValue(p);
860
994
  if (value !== void 0) props.push(Object.freeze({ code: pcode, value }));
861
995
  }
862
996
  if (parentCode !== void 0 && !props.some((p) => p.code === "parent" && p.value === parentCode)) {
@@ -919,9 +1053,21 @@ var ICD10CM_ORDER_FILE_FIELDS = Object.freeze({
919
1053
  Object.freeze({ name: "shortDescription", slice: Object.freeze({ start: 16, end: 76 }) })
920
1054
  ])
921
1055
  });
1056
+ function requirePropertyFields(fields) {
1057
+ for (const p of fields.properties ?? []) {
1058
+ const entry = p;
1059
+ if (typeof entry !== "object" || entry === null) {
1060
+ throw new TerminologyError(
1061
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
1062
+ "fixed-width source: a configured property field is not an object"
1063
+ );
1064
+ }
1065
+ }
1066
+ }
922
1067
  function parseFixedWidth(source) {
923
1068
  const statusMap = source.statusMap ?? defaultStatusMapper;
924
1069
  const fields = source.fields;
1070
+ requirePropertyFields(fields);
925
1071
  const concepts = /* @__PURE__ */ new Map();
926
1072
  const warnings = [];
927
1073
  const lines = source.content.split(/\r?\n/);
@@ -983,7 +1129,16 @@ function parseRrfLine(line) {
983
1129
  function maxColumn(source) {
984
1130
  const { code, display, status, properties } = source.columns;
985
1131
  let max = Math.max(code, display, status ?? 0);
986
- for (const p of properties ?? []) max = Math.max(max, p.column);
1132
+ for (const p of properties ?? []) {
1133
+ const entry = p;
1134
+ if (typeof entry !== "object" || entry === null) {
1135
+ throw new TerminologyError(
1136
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
1137
+ "RRF source: a configured property column is not an object"
1138
+ );
1139
+ }
1140
+ max = Math.max(max, p.column);
1141
+ }
987
1142
  return max;
988
1143
  }
989
1144
  function cell(cells, index) {
@@ -1044,6 +1199,10 @@ function parseRrf(source) {
1044
1199
  }
1045
1200
 
1046
1201
  // src/codesystem/load.ts
1202
+ var loadedConcepts = /* @__PURE__ */ new WeakSet();
1203
+ function isLoadedConcepts(concepts) {
1204
+ return loadedConcepts.has(concepts);
1205
+ }
1047
1206
  function loadCodeSystem(source) {
1048
1207
  let concepts;
1049
1208
  let warnings;
@@ -1082,6 +1241,7 @@ function loadCodeSystem(source) {
1082
1241
  }
1083
1242
  out.count = concepts.size;
1084
1243
  out.concepts = frozenMap(concepts, "a loaded CodeSystem's concepts map");
1244
+ loadedConcepts.add(out.concepts);
1085
1245
  out.warnings = Object.freeze(warnings);
1086
1246
  return Object.freeze(out);
1087
1247
  }
@@ -1284,8 +1444,11 @@ function directParents(concept) {
1284
1444
  return parents;
1285
1445
  }
1286
1446
  function buildSubsumption(cs) {
1447
+ return subsumptionOver(cs.concepts);
1448
+ }
1449
+ function subsumptionOver(concepts) {
1287
1450
  const parentsOf = /* @__PURE__ */ new Map();
1288
- for (const concept of cs.concepts.values()) {
1451
+ for (const concept of concepts.values()) {
1289
1452
  const parents = directParents(concept);
1290
1453
  if (parents.length > 0) parentsOf.set(concept.code, parents);
1291
1454
  }
@@ -1306,6 +1469,17 @@ function buildSubsumption(cs) {
1306
1469
  };
1307
1470
  return { ancestorsOf };
1308
1471
  }
1472
+ var loadedSubsumption = /* @__PURE__ */ new WeakMap();
1473
+ function subsumptionOf(cs) {
1474
+ const concepts = cs.concepts;
1475
+ if (!isLoadedConcepts(concepts)) return subsumptionOver(concepts);
1476
+ let sub = loadedSubsumption.get(concepts);
1477
+ if (sub === void 0) {
1478
+ sub = subsumptionOver(concepts);
1479
+ loadedSubsumption.set(concepts, sub);
1480
+ }
1481
+ return sub;
1482
+ }
1309
1483
  function isA(sub, code, anchor) {
1310
1484
  return code === anchor || sub.ancestorsOf(code).has(anchor);
1311
1485
  }
@@ -1390,8 +1564,7 @@ function makeCoding(system, code, display, version) {
1390
1564
  return coding(init);
1391
1565
  }
1392
1566
  function cannotExpand(detail, path) {
1393
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1394
- if (path !== void 0) d.path = path;
1567
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1395
1568
  return Object.freeze(d);
1396
1569
  }
1397
1570
  function truncated(detail, path) {
@@ -1405,9 +1578,9 @@ function truncated(detail, path) {
1405
1578
  function enumeratedUndefined(detail, path) {
1406
1579
  const d = {
1407
1580
  code: "TERM_VALUESET_ENUMERATED_CODE_UNDEFINED",
1408
- detail
1581
+ detail,
1582
+ path
1409
1583
  };
1410
- if (path !== void 0) d.path = path;
1411
1584
  return Object.freeze(d);
1412
1585
  }
1413
1586
  function expansionIncompleteDetail(unclosed) {
@@ -1441,7 +1614,8 @@ function screenActive(members, component, ctx) {
1441
1614
  }
1442
1615
  function underPath(d, prefix) {
1443
1616
  const out = { code: d.code, detail: d.detail };
1444
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1617
+ if (d.path === void 0) out.path = prefix;
1618
+ else out.path = `${prefix}/${d.path}`;
1445
1619
  return Object.freeze(out);
1446
1620
  }
1447
1621
  function intersectInto(into, other) {
@@ -1637,8 +1811,7 @@ function expand(vs, ctx = {}) {
1637
1811
 
1638
1812
  // src/valueset/validate.ts
1639
1813
  function cannotExpand2(detail, path) {
1640
- const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail };
1641
- if (path !== void 0) d.path = path;
1814
+ const d = { code: "TERM_VALUESET_CANNOT_EXPAND", detail, path };
1642
1815
  return Object.freeze(d);
1643
1816
  }
1644
1817
  function expansionIncompleteDetail2(unclosed) {
@@ -1656,7 +1829,8 @@ function usableRelease2(component, system, ctx) {
1656
1829
  }
1657
1830
  function underPath2(d, prefix) {
1658
1831
  const out = { code: d.code, detail: d.detail };
1659
- out.path = d.path === void 0 ? prefix : `${prefix}/${d.path}`;
1832
+ if (d.path === void 0) out.path = prefix;
1833
+ else out.path = `${prefix}/${d.path}`;
1660
1834
  return Object.freeze(out);
1661
1835
  }
1662
1836
  function matchComponent(target, component, ctx, visited, path, activeOnly = false) {
@@ -1708,7 +1882,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1708
1882
  baseComplete = false;
1709
1883
  } else {
1710
1884
  const concpt = cs.concepts.get(target.code);
1711
- baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, buildSubsumption(cs)).matched;
1885
+ baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, subsumptionOf(cs)).matched;
1712
1886
  }
1713
1887
  } else {
1714
1888
  baseMatched = cs.concepts.get(target.code) !== void 0;
@@ -1716,10 +1890,13 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1716
1890
  }
1717
1891
  }
1718
1892
  if (system !== void 0 && target.system === void 0 && baseComplete) baseMatched = false;
1893
+ if (baseMatched === null && !hasVs) return { matched: false, complete: true, diagnostics };
1719
1894
  let refDefiniteFalse = false;
1720
1895
  let refUndetermined = false;
1721
1896
  if (hasVs) {
1897
+ let refsRead = 0;
1722
1898
  for (const [i, url] of valueSet2.entries()) {
1899
+ refsRead++;
1723
1900
  const refPath = `${path}.valueSet[${String(i)}]`;
1724
1901
  if (visited.has(url)) {
1725
1902
  diagnostics.push(cannotExpand2("cyclic value set reference", refPath));
@@ -1740,6 +1917,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1740
1917
  refDefiniteFalse = true;
1741
1918
  }
1742
1919
  }
1920
+ if (refsRead === 0) refDefiniteFalse = true;
1743
1921
  }
1744
1922
  const baseDefiniteFalse = baseMatched === false && baseComplete;
1745
1923
  const baseDefiniteTrue = baseMatched === null || baseMatched === true && baseComplete;
@@ -3991,7 +4169,32 @@ function atBoundary(s, pos) {
3991
4169
  const c = s[pos];
3992
4170
  return c === "." || c === "/" || c === ")";
3993
4171
  }
4172
+ function prefixRank(m) {
4173
+ return m.prefix ? 1 : 0;
4174
+ }
4175
+ function byPreference(a, b) {
4176
+ return b.len - a.len || prefixRank(a) - prefixRank(b);
4177
+ }
3994
4178
  function simpleMatches(essence, s, pos) {
4179
+ if (typeof s !== "string") return simpleMatchesByScan(essence, s, pos);
4180
+ const index = indexOf(essence);
4181
+ const out = [];
4182
+ for (const atom of index.atoms.get(s.charCodeAt(pos)) ?? NONE) {
4183
+ if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
4184
+ }
4185
+ for (const prefix of index.prefixes.get(s.charCodeAt(pos)) ?? NONE) {
4186
+ if (!s.startsWith(prefix.code, pos)) continue;
4187
+ const rem = pos + prefix.code.length;
4188
+ for (const atom of index.metricAtoms.get(s.charCodeAt(rem)) ?? NONE) {
4189
+ if (s.startsWith(atom.code, rem)) {
4190
+ out.push({ prefix, atom, len: prefix.code.length + atom.code.length });
4191
+ }
4192
+ }
4193
+ }
4194
+ out.sort(byPreference);
4195
+ return out;
4196
+ }
4197
+ function simpleMatchesByScan(essence, s, pos) {
3995
4198
  const out = [];
3996
4199
  for (const atom of essence.atoms) {
3997
4200
  if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
@@ -4005,9 +4208,33 @@ function simpleMatches(essence, s, pos) {
4005
4208
  }
4006
4209
  }
4007
4210
  }
4008
- out.sort((a, b) => b.len - a.len || (a.prefix ? 1 : 0) - (b.prefix ? 1 : 0));
4211
+ out.sort(byPreference);
4212
+ return out;
4213
+ }
4214
+ var NONE = Object.freeze([]);
4215
+ var firstCharIndexes = /* @__PURE__ */ new WeakMap();
4216
+ function byFirstChar(items) {
4217
+ const out = /* @__PURE__ */ new Map();
4218
+ for (const item of items) {
4219
+ const first = item.code.charCodeAt(0);
4220
+ const list = out.get(first);
4221
+ if (list === void 0) out.set(first, [item]);
4222
+ else list.push(item);
4223
+ }
4009
4224
  return out;
4010
4225
  }
4226
+ function indexOf(essence) {
4227
+ let index = firstCharIndexes.get(essence);
4228
+ if (index === void 0) {
4229
+ index = {
4230
+ atoms: byFirstChar(essence.atoms),
4231
+ metricAtoms: byFirstChar(essence.atoms.filter((a) => a.metric)),
4232
+ prefixes: byFirstChar(essence.prefixes)
4233
+ };
4234
+ firstCharIndexes.set(essence, index);
4235
+ }
4236
+ return index;
4237
+ }
4011
4238
  var MAX_PAREN_DEPTH = 100;
4012
4239
  function parseExponent(cur) {
4013
4240
  const { s } = cur;
@@ -4040,8 +4267,8 @@ function parseAnnotation(cur) {
4040
4267
  return { ok: false, reason: "annotation is not closed with '}'" };
4041
4268
  }
4042
4269
  function parseSimpleUnit(cur) {
4043
- const candidates = simpleMatches(cur.essence, cur.s, cur.pos);
4044
- for (const cand of candidates) {
4270
+ const candidates2 = simpleMatches(cur.essence, cur.s, cur.pos);
4271
+ for (const cand of candidates2) {
4045
4272
  const save = cur.pos;
4046
4273
  cur.pos = save + cand.len;
4047
4274
  const exp = parseExponent(cur);
@@ -4267,57 +4494,62 @@ function isNoMapSentinel(target) {
4267
4494
  return target === NO_DX || target === NO_PCS;
4268
4495
  }
4269
4496
  function decodeFlags(raw) {
4270
- if (!/^[0-9]{5}$/.test(raw)) return void 0;
4271
4497
  return Object.freeze({
4272
- approximate: raw[0] === "1",
4273
- noMap: raw[1] === "1",
4274
- combination: raw[2] === "1",
4275
- scenario: Number(raw[3]),
4276
- choiceList: Number(raw[4]),
4498
+ approximate: raw.charCodeAt(0) === DIGIT_ONE,
4499
+ noMap: raw.charCodeAt(1) === DIGIT_ONE,
4500
+ combination: raw.charCodeAt(2) === DIGIT_ONE,
4501
+ scenario: raw.charCodeAt(3) - DIGIT_ZERO,
4502
+ choiceList: raw.charCodeAt(4) - DIGIT_ZERO,
4277
4503
  raw
4278
4504
  });
4279
4505
  }
4506
+ var DIGIT_ZERO = 48;
4507
+ var DIGIT_ONE = 49;
4508
+ var DIGIT_NINE = 57;
4509
+ var SPACE = 32;
4510
+ var NON_ASCII_SPACE = /^\s$/;
4511
+ function isSpace(c) {
4512
+ if (c < 128) return c === SPACE || c >= 9 && c <= 13;
4513
+ return NON_ASCII_SPACE.test(String.fromCharCode(c));
4514
+ }
4280
4515
  function loadGems(source) {
4281
4516
  const entries = /* @__PURE__ */ new Map();
4282
4517
  const warnings = [];
4283
4518
  let count = 0;
4284
- const lines = source.content.split(/\r?\n/);
4285
- for (let i = 0; i < lines.length; i++) {
4286
- const raw = lines[i];
4287
- if (raw === void 0) continue;
4288
- const trimmed = raw.trim();
4289
- if (trimmed === "") continue;
4290
- const fields = trimmed.split(/\s+/);
4291
- if (fields.length !== 3) {
4519
+ const sink = {
4520
+ flags: /* @__PURE__ */ new Map(),
4521
+ malformed: (lineNo, fieldCount) => {
4292
4522
  warnings.push(
4293
4523
  Object.freeze({
4294
4524
  code: "TERM_GEM_MALFORMED_ROW",
4295
- line: i + 1,
4296
- detail: `expected 3 whitespace-delimited fields, found ${String(fields.length)}`
4525
+ line: lineNo,
4526
+ detail: `expected 3 whitespace-delimited fields, found ${String(fieldCount)}`
4297
4527
  })
4298
4528
  );
4299
- continue;
4300
- }
4301
- const [src, tgt, flagStr] = fields;
4302
- const flags = decodeFlags(flagStr);
4303
- if (flags === void 0) {
4304
- warnings.push(
4305
- Object.freeze({
4306
- code: "TERM_GEM_MALFORMED_ROW",
4307
- line: i + 1,
4308
- detail: "flag field is not a 5-digit code"
4309
- })
4529
+ },
4530
+ line: (lineNo, src, tgt, flags) => {
4531
+ if (flags === void 0) {
4532
+ warnings.push(
4533
+ Object.freeze({
4534
+ code: "TERM_GEM_MALFORMED_ROW",
4535
+ line: lineNo,
4536
+ detail: "flag field is not a 5-digit code"
4537
+ })
4538
+ );
4539
+ return;
4540
+ }
4541
+ const frozen = Object.freeze(
4542
+ isNoMapSentinel(tgt) ? { source: src, flags } : { source: src, flags, target: tgt }
4310
4543
  );
4311
- continue;
4544
+ const list = entries.get(src);
4545
+ if (list) list.push(frozen);
4546
+ else entries.set(src, [frozen]);
4547
+ count++;
4312
4548
  }
4313
- const entry = { source: src, flags };
4314
- if (!isNoMapSentinel(tgt)) entry.target = tgt;
4315
- const frozen = Object.freeze(entry);
4316
- const list = entries.get(src);
4317
- if (list) list.push(frozen);
4318
- else entries.set(src, [frozen]);
4319
- count++;
4320
- }
4549
+ };
4550
+ const content = source.content;
4551
+ if (typeof content === "string") readLinesInPlace(content, sink);
4552
+ else readLinesBySplit(content, sink);
4321
4553
  for (const list of entries.values()) Object.freeze(list);
4322
4554
  return Object.freeze({
4323
4555
  direction: source.direction,
@@ -4327,6 +4559,90 @@ function loadGems(source) {
4327
4559
  warnings: Object.freeze(warnings)
4328
4560
  });
4329
4561
  }
4562
+ function sharedFlags(sink, text, from, to) {
4563
+ const key = fiveDigitsAt(text, from, to);
4564
+ if (key === -1) return void 0;
4565
+ let flags = sink.flags.get(key);
4566
+ if (flags === void 0) {
4567
+ flags = decodeFlags(text.slice(from, to));
4568
+ sink.flags.set(key, flags);
4569
+ }
4570
+ return flags;
4571
+ }
4572
+ function fiveDigitsAt(content, from, to) {
4573
+ if (to - from !== 5) return -1;
4574
+ let value = 0;
4575
+ for (let i = from; i < to; i++) {
4576
+ const c = content.charCodeAt(i);
4577
+ if (c < DIGIT_ZERO || c > DIGIT_NINE) return -1;
4578
+ value = value * 10 + (c - DIGIT_ZERO);
4579
+ }
4580
+ return value;
4581
+ }
4582
+ function readLinesInPlace(content, sink) {
4583
+ const n = content.length;
4584
+ let lineNo = 0;
4585
+ for (let start = 0; start <= n; ) {
4586
+ let end = content.indexOf("\n", start);
4587
+ if (end === -1) end = n;
4588
+ lineNo++;
4589
+ let fieldCount = 0;
4590
+ let srcStart = 0;
4591
+ let srcEnd = 0;
4592
+ let tgtStart = 0;
4593
+ let tgtEnd = 0;
4594
+ let flagStart = 0;
4595
+ let flagEnd = 0;
4596
+ for (let i = start; i < end; ) {
4597
+ if (isSpace(content.charCodeAt(i))) {
4598
+ i++;
4599
+ continue;
4600
+ }
4601
+ const from = i;
4602
+ while (i < end && !isSpace(content.charCodeAt(i))) i++;
4603
+ fieldCount++;
4604
+ if (fieldCount === 1) {
4605
+ srcStart = from;
4606
+ srcEnd = i;
4607
+ } else if (fieldCount === 2) {
4608
+ tgtStart = from;
4609
+ tgtEnd = i;
4610
+ } else if (fieldCount === 3) {
4611
+ flagStart = from;
4612
+ flagEnd = i;
4613
+ }
4614
+ }
4615
+ start = end + 1;
4616
+ if (fieldCount === 0) continue;
4617
+ if (fieldCount !== 3) {
4618
+ sink.malformed(lineNo, fieldCount);
4619
+ continue;
4620
+ }
4621
+ const flags = sharedFlags(sink, content, flagStart, flagEnd);
4622
+ sink.line(
4623
+ lineNo,
4624
+ ownString(content.slice(srcStart, srcEnd)),
4625
+ ownString(content.slice(tgtStart, tgtEnd)),
4626
+ flags
4627
+ );
4628
+ }
4629
+ }
4630
+ function readLinesBySplit(content, sink) {
4631
+ const lines = content.split(/\r?\n/);
4632
+ for (let i = 0; i < lines.length; i++) {
4633
+ const raw = lines[i];
4634
+ if (raw === void 0) continue;
4635
+ const trimmed = raw.trim();
4636
+ if (trimmed === "") continue;
4637
+ const fields = trimmed.split(/\s+/);
4638
+ if (fields.length !== 3) {
4639
+ sink.malformed(i + 1, fields.length);
4640
+ continue;
4641
+ }
4642
+ const [src, tgt, flagStr] = fields;
4643
+ sink.line(i + 1, src, tgt, sharedFlags(sink, flagStr, 0, flagStr.length));
4644
+ }
4645
+ }
4330
4646
  function buildCombinations(entries) {
4331
4647
  const combo = entries.filter((e) => e.flags.combination && e.target !== void 0);
4332
4648
  if (combo.length === 0) return void 0;
@@ -4410,7 +4726,7 @@ var MAP_CATEGORIES = {
4410
4726
  NO_MAP: "447638001",
4411
4727
  /** `447639009`: "Map of source concept is context dependent" (needs runtime patient context). */
4412
4728
  CONTEXT_DEPENDENT: "447639009",
4413
- /** `447640006`: "Source concept is ambiguous" (surface candidates, never pick one). */
4729
+ /** `447640006`: "Source concept is ambiguous" (carried through on the result; the rule decides). */
4414
4730
  AMBIGUOUS: "447640006"
4415
4731
  };
4416
4732
  var NO_MAP_CATEGORIES = /* @__PURE__ */ new Set([MAP_CATEGORIES.NO_MAP]);
@@ -4543,8 +4859,15 @@ function evaluateConjunct(clause, context) {
4543
4859
  const bound = Number(ageMatch[2]);
4544
4860
  if (context.ageYears === void 0 || !Number.isFinite(bound)) return "unknown";
4545
4861
  const age = context.ageYears;
4546
- const ok = op === "<=" ? age <= bound : op === ">=" ? age >= bound : op === "<" ? age < bound : op === ">" ? age > bound : op === "=" ? age === bound : void 0;
4547
- if (ok === void 0) return "unknown";
4862
+ let ok;
4863
+ if (op === "<=") ok = age <= bound;
4864
+ else if (op === ">=") ok = age >= bound;
4865
+ else if (op === "<") ok = age < bound;
4866
+ else if (op === ">") ok = age > bound;
4867
+ else {
4868
+ if (op === "=") ok = age === bound;
4869
+ else return "unknown";
4870
+ }
4548
4871
  return ok ? "match" : "no-match";
4549
4872
  }
4550
4873
  return "unknown";
@@ -4963,7 +5286,8 @@ function jaccard(a, b) {
4963
5286
  let inter = 0;
4964
5287
  for (const t of a) if (b.has(t)) inter++;
4965
5288
  const union = a.size + b.size - inter;
4966
- return union === 0 ? 0 : inter / union;
5289
+ if (union === 0) return 0;
5290
+ return inter / union;
4967
5291
  }
4968
5292
  function approximateMatch(graph, query, options = {}) {
4969
5293
  const minScore = options.minScore ?? 0.34;
@@ -4980,7 +5304,7 @@ function approximateMatch(graph, query, options = {}) {
4980
5304
  }
4981
5305
 
4982
5306
  // src/index.ts
4983
- var VERSION = "0.1.5";
5307
+ var VERSION = "0.1.7";
4984
5308
 
4985
5309
  export { COMPLEX_MAP_SOURCE_SYSTEM, DIAGNOSTIC_CODES, FATAL_CODES, HL7_V2_OID_ROOT, HL7_V2_URL_PREFIX, ICD10CM_ORDER_FILE_FIELDS, ICD10CM_SYSTEM, ICD9CM_SYSTEM, MAP_CATEGORIES, NO_MAP_CATEGORIES, RELA, RELA_INVERSE, RXNORM_SYSTEM, SNOMEDCT_SYSTEM, SYSTEM_IDENTITIES, TERM_TYPES, TerminologyError, VERSION, applyComplexMap, applyGem, approximateMatch, asTermType, brandsFor, buildSubsumption, codeableConcept, coding, consistsOf, defaultStatusMapper, doseFormsOf, expand, genericFor, getArray, getBoolean, getConcept, getNumber, getString, ingredientsOf, invertGem, isA, isJsonObject, isSynonymTermType, isUnknownSystem, loadCodeSystem, loadComplexMap, loadConceptMap, loadGems, loadRxNormGraph, loadUcumEssence, loadValueSet, lookup, makeStatus, matchesFilter, parseCsv, parseCsvSource, parseFhirCodeSystem, parseFixedWidth, parseRrf, parseRrfLine, parseUcum, reduce, relatedByRela, resolveNdc, resolveSystem, sliceField, translate, ucumEqual, unsupportedOps, validateCode, validateCodeInValueSet, validateUcum };
4986
5310
  //# sourceMappingURL=index.mjs.map