Map ICD-9-CM and ICD-10-CM codes with the GEMs
The CMS General Equivalence Mappings (GEMs) are public-domain reference mappings between ICD-9-CM and ICD-10-CM. CMS ships one file per direction: the forward file maps ICD-9-CM to ICD-10-CM and the backward file maps ICD-10-CM to ICD-9-CM. You supply the file; the engine reads it and applies it in its own direction. The rows below are synthetic, in the GEM file format.
Load a GEM file
Each line is source target flags, separated by whitespace. Say which direction the file maps.
import { loadGems } from "@cosyte/terminology";
const gems = loadGems({
direction: "9-to-10",
version: "2018",
content: ["25000 E119 10000", "V290 NoDx 11000", "this line is not a GEM row"].join("\n"),
});
gems.direction; // => "9-to-10"
gems.count; // => 2
gems.warnings; // => [{ code: "TERM_GEM_MALFORMED_ROW", line: 3, detail: "expected 3 whitespace-delimited fields, found 7" }]
A line that is not three fields, or whose flags are not five digits, is skipped and reported as
TERM_GEM_MALFORMED_ROW. Loading a GEM file never
throws.
Apply it and read the candidates
import { loadGems, applyGem } from "@cosyte/terminology";
const gems = loadGems({ direction: "9-to-10", content: "25000 E119 10000\n" });
const result = applyGem(gems, "25000");
result.mapped; // => true
if (result.mapped) {
result.entries.map((entry) => entry.target); // => ["E119"]
result.entries[0]?.flags; // => { approximate: true, noMap: false, combination: false, scenario: 0, choiceList: 0, raw: "10000" }
}
Every entry for the source comes back, in file order, with its five flags decoded: approximate,
noMap, combination, scenario and choiceList, and raw, the flag field as written. Most GEM
entries are approximate. A GEM hit is a candidate to review, not an equivalence: CMS calls the GEMs
reference mappings, not crosswalks.
Handle a No-Map and an absent code
A source the steward marked No-Map (target NoDx, or NoPCS in the procedure files) is a typed
TERM_CROSSWALK_NO_MAP. A source that is not in the
file at all is TERM_CROSSWALK_UNMAPPED. Neither
returns a target.
import { loadGems, applyGem } from "@cosyte/terminology";
const gems = loadGems({ direction: "9-to-10", content: "V290 NoDx 11000\n" });
const noMap = applyGem(gems, "V290");
noMap.mapped; // => false
!noMap.mapped && noMap.noMap; // => true
!noMap.mapped && noMap.code; // => "TERM_CROSSWALK_NO_MAP"
const absent = applyGem(gems, "9999");
!absent.mapped && absent.code; // => "TERM_CROSSWALK_UNMAPPED"
The two are kept apart on purpose: "the steward says there is no target" is a different fact from "this code is not in the file you loaded".
Build a combination
Some sources need more than one target code together. Their entries carry the combination flag, a
scenario number and a choice-list number, and the result groups them for you in combinations. A
valid set of codes takes one target from each choice list of one scenario. The codes in this sample
are invented.
import { loadGems, applyGem } from "@cosyte/terminology";
const gems = loadGems({
direction: "9-to-10",
content: ["X9901 QX01 10111", "X9901 QX02 10112", "X9901 QX03 10112"].join("\n"),
});
const result = applyGem(gems, "X9901");
result.mapped && result.combinations; // => [{ scenario: 1, choiceLists: [{ choiceList: 1, targets: ["QX01"] }, { choiceList: 2, targets: ["QX02", "QX03"] }] }]
Here scenario 1 gives two valid sets: QX01 with QX02, or QX01 with QX03. A source that is
not a combination has no combinations.
Map in the other direction
Load the other file. The forward and backward GEMs are separate files with different content, and
invertGem exists only to refuse:
import { loadGems, invertGem } from "@cosyte/terminology";
const forward = loadGems({ direction: "9-to-10", content: "25000 E119 10000\n" });
invertGem(forward); // throws TERM_MAP_NOT_INVERTIBLE
It always throws a TerminologyError with
TERM_MAP_NOT_INVERTIBLE, whose message names the
file to load instead. How the crosswalk resolvers read a map explains why.