mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(labs): never present a converted reading's printed range in the wrong unit
A converted reading's bounds move with its value, but the printed range text stays in the unit the report used, and every reader preferred that text: the list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor report and the FHIR referenceRange.text did the same. The text is still stored verbatim. The range resolver now takes the reading's sourceUnit (as a required field, so no reader can forget to select it) and, for a converted reading, leaves the window's text empty so every surface states the converted bounds in the marker's unit, and hands the printed text over separately with its unit. The lab DTO carries it as printedReferenceText, and the note beside a reading shows it as "as printed in mmol/L: ...".
This commit is contained in:
1 parent
cc0313c955
commit
47a91d6922
29 files changed
+365
-20
No files matched your search
@@ -9,6 +9,7 @@ const NO_SOURCE = {
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: null,
|
||||
sourceReferenceText: null,
|
||||
sourceUnit: null,
|
||||
};
|
||||
|
||||
describe("resolveEffectiveReferenceRange", () => {
|
||||
@@ -36,6 +37,7 @@ describe("resolveEffectiveReferenceRange", () => {
|
||||
sourceReferenceLow: 3.9,
|
||||
sourceReferenceHigh: 5.4,
|
||||
sourceReferenceText: "3,9 - 5,4",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff).toMatchObject({
|
||||
low: 3.9,
|
||||
@@ -53,6 +55,7 @@ describe("resolveEffectiveReferenceRange", () => {
|
||||
sourceReferenceLow: 3.5,
|
||||
sourceReferenceHigh: 5,
|
||||
sourceReferenceText: "3,5 - 5,0",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff.origin).toBe("source");
|
||||
expect(eff.divergesFromCatalog).toBe(false);
|
||||
@@ -65,6 +68,7 @@ describe("resolveEffectiveReferenceRange", () => {
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: 4.2,
|
||||
sourceReferenceText: "< 4,2",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff).toMatchObject({
|
||||
low: null,
|
||||
@@ -81,6 +85,7 @@ describe("resolveEffectiveReferenceRange", () => {
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: null,
|
||||
sourceReferenceText: "negativ",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff).toMatchObject({
|
||||
low: 3.5,
|
||||
@@ -96,12 +101,57 @@ describe("resolveEffectiveReferenceRange", () => {
|
||||
sourceReferenceLow: 3.9,
|
||||
sourceReferenceHigh: 5.4,
|
||||
sourceReferenceText: "3,9 - 5,4",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff.origin).toBe("source");
|
||||
expect(eff.divergesFromCatalog).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a converted reading's printed text", () => {
|
||||
// 5.6 mmol/L stored as mg/dL, with the printed 3.9 to 5.5 mmol/L window
|
||||
// converted alongside; the text stays as the report printed it.
|
||||
const CONVERTED = {
|
||||
sourceReferenceLow: 3.9 * 18.0182,
|
||||
sourceReferenceHigh: 5.5 * 18.0182,
|
||||
sourceReferenceText: "3,9 - 5,5",
|
||||
sourceUnit: "mmol/L",
|
||||
};
|
||||
|
||||
it("is never the window's text, so no statement mixes two units", () => {
|
||||
const eff = resolveEffectiveReferenceRange(70, 100, CONVERTED);
|
||||
expect(eff.origin).toBe("source");
|
||||
expect(eff.low).toBeCloseTo(70.27, 2);
|
||||
expect(eff.sourceText).toBeNull();
|
||||
});
|
||||
|
||||
it("is kept as evidence with the unit it is written in", () => {
|
||||
const eff = resolveEffectiveReferenceRange(70, 100, CONVERTED);
|
||||
expect(eff.printed).toEqual({ text: "3,9 - 5,5", unit: "mmol/L" });
|
||||
});
|
||||
|
||||
it("stays the window's text for a reading stored as stated", () => {
|
||||
const eff = resolveEffectiveReferenceRange(70, 100, {
|
||||
...CONVERTED,
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(eff.sourceText).toBe("3,9 - 5,5");
|
||||
expect(eff.printed).toBeNull();
|
||||
});
|
||||
|
||||
it("moves an unreadable printed text the same way", () => {
|
||||
const eff = resolveEffectiveReferenceRange(70, 100, {
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: null,
|
||||
sourceReferenceText: "siehe Befund",
|
||||
sourceUnit: "mmol/L",
|
||||
});
|
||||
expect(eff.origin).toBe("catalog");
|
||||
expect(eff.sourceText).toBeNull();
|
||||
expect(eff.printed).toEqual({ text: "siehe Befund", unit: "mmol/L" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyAgainstEffectiveRange", () => {
|
||||
it("judges a reading by the source window, not the catalog band", () => {
|
||||
// 5.2 sits inside the catalog band (3.5–5.0 would call it high) but the
|
||||
@@ -110,6 +160,7 @@ describe("classifyAgainstEffectiveRange", () => {
|
||||
sourceReferenceLow: 3.9,
|
||||
sourceReferenceHigh: 5.4,
|
||||
sourceReferenceText: "3,9 - 5,4",
|
||||
sourceUnit: null,
|
||||
});
|
||||
expect(classifyAgainstEffectiveRange(5.2, eff)).toBe("in-range");
|
||||
expect(classifyAgainstEffectiveRange(3.7, eff)).toBe("below");
|
||||
|
||||
@@ -20,6 +20,8 @@ const baseRow: LabRow = {
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: null,
|
||||
sourceReferenceText: null,
|
||||
sourceValue: null,
|
||||
sourceUnit: null,
|
||||
takenAt: new Date("2026-01-01T08:00:00.000Z"),
|
||||
source: "MANUAL",
|
||||
biomarkerId: "bm_1",
|
||||
|
||||
@@ -65,11 +65,23 @@ export function formatReferenceRange(
|
||||
/**
|
||||
* The reference range a lab report printed beside ONE reading, as stored on
|
||||
* `LabResult.sourceReference*`.
|
||||
*
|
||||
* `sourceUnit` is set when the reading was converted into its marker's unit:
|
||||
* the bounds were converted with the value, but the text is the report's own
|
||||
* and stays in the unit the report printed. It is required, so a reader cannot
|
||||
* forget to select it and present that text beside numbers in another unit.
|
||||
*/
|
||||
export interface SourceReferenceRange {
|
||||
sourceReferenceLow: number | null;
|
||||
sourceReferenceHigh: number | null;
|
||||
sourceReferenceText: string | null;
|
||||
sourceUnit: string | null;
|
||||
}
|
||||
|
||||
/** A converted reading's printed range text, with the unit it is written in. */
|
||||
export interface PrintedReferenceRange {
|
||||
text: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
/** Where the bounds a reading is judged against came from. */
|
||||
@@ -84,8 +96,19 @@ export interface EffectiveReferenceRange {
|
||||
/** The catalog band, kept alongside so a surface can show both. */
|
||||
catalogLow: number | null;
|
||||
catalogHigh: number | null;
|
||||
/** The printed window, kept verbatim. Null when the report stated none. */
|
||||
/**
|
||||
* The printed window, kept verbatim, in the reading's unit. Null when the
|
||||
* report stated none, and null for a converted reading: its text is in the
|
||||
* unit the report printed, not the unit of the value and bounds beside it,
|
||||
* and moves to `printed`.
|
||||
*/
|
||||
sourceText: string | null;
|
||||
/**
|
||||
* For a converted reading only: the window as the report printed it and the
|
||||
* unit it is in. Evidence, never a statement about the stored numbers; a
|
||||
* surface that shows it names the unit with it.
|
||||
*/
|
||||
printed: PrintedReferenceRange | null;
|
||||
/**
|
||||
* True when the source window is in force AND the catalog states a window
|
||||
* that differs from it. This is the case a reader must be told about: the
|
||||
@@ -132,7 +155,15 @@ export function resolveEffectiveReferenceRange(
|
||||
const cHigh = catalogHigh ?? null;
|
||||
const sLow = source?.sourceReferenceLow ?? null;
|
||||
const sHigh = source?.sourceReferenceHigh ?? null;
|
||||
const sourceText = source?.sourceReferenceText ?? null;
|
||||
const verbatim = source?.sourceReferenceText ?? null;
|
||||
const sourceUnit = source?.sourceUnit ?? null;
|
||||
// A converted reading's text is in the printed unit; it is never presented as
|
||||
// the window of the converted value.
|
||||
const sourceText = sourceUnit === null ? verbatim : null;
|
||||
const printed: PrintedReferenceRange | null =
|
||||
sourceUnit !== null && verbatim !== null
|
||||
? { text: verbatim, unit: sourceUnit }
|
||||
: null;
|
||||
|
||||
if (sLow !== null || sHigh !== null) {
|
||||
return {
|
||||
@@ -142,6 +173,7 @@ export function resolveEffectiveReferenceRange(
|
||||
catalogLow: cLow,
|
||||
catalogHigh: cHigh,
|
||||
sourceText,
|
||||
printed,
|
||||
divergesFromCatalog:
|
||||
(cLow !== null || cHigh !== null) &&
|
||||
!sameWindow(sLow, sHigh, cLow, cHigh),
|
||||
@@ -155,6 +187,7 @@ export function resolveEffectiveReferenceRange(
|
||||
catalogLow: cLow,
|
||||
catalogHigh: cHigh,
|
||||
sourceText,
|
||||
printed,
|
||||
divergesFromCatalog: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
* - `catalogReferenceLow` / `catalogReferenceHigh` — the catalog band, so a
|
||||
* surface can paint both and show where they part.
|
||||
* - `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText` —
|
||||
* what the report printed, the last verbatim.
|
||||
* what the report printed, the last verbatim. For a reading converted into
|
||||
* its marker's unit the bounds were converted with the value, but the text
|
||||
* is still in the unit the report printed, so it is never sent as
|
||||
* `sourceReferenceText` (that would put two units in one statement): it
|
||||
* travels as `printedReferenceText`, in `sourceUnit`.
|
||||
*
|
||||
* `referenceOrigin` names which window is in force and
|
||||
* `referenceDivergesFromCatalog` flags the case a reader must be told about.
|
||||
@@ -80,8 +84,8 @@ export interface LabRow {
|
||||
* When the reading was converted into the marker's unit: the value and unit
|
||||
* as the report printed them. Both null for a reading stored as stated.
|
||||
*/
|
||||
sourceValue?: number | null;
|
||||
sourceUnit?: string | null;
|
||||
sourceValue: number | null;
|
||||
sourceUnit: string | null;
|
||||
noteEncrypted: Uint8Array | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -97,6 +101,7 @@ export type LabFieldRow = Pick<
|
||||
| "sourceReferenceLow"
|
||||
| "sourceReferenceHigh"
|
||||
| "sourceReferenceText"
|
||||
| "sourceUnit"
|
||||
| "panel"
|
||||
| "biomarkerId"
|
||||
>;
|
||||
@@ -115,7 +120,13 @@ export interface ResolvedLabFields {
|
||||
/** What the source report printed for this reading. */
|
||||
sourceReferenceLow: number | null;
|
||||
sourceReferenceHigh: number | null;
|
||||
/** Null for a converted reading; see `printedReferenceText`. */
|
||||
sourceReferenceText: string | null;
|
||||
/**
|
||||
* A converted reading's printed range text, verbatim, in `sourceUnit` (the
|
||||
* unit the report printed); null for every other reading.
|
||||
*/
|
||||
printedReferenceText: string | null;
|
||||
referenceOrigin: ReferenceRangeOrigin;
|
||||
referenceDivergesFromCatalog: boolean;
|
||||
/** The resolved window as one value, for callers that pass it on. */
|
||||
@@ -172,6 +183,7 @@ export function resolveLabFields(
|
||||
sourceReferenceLow: row.sourceReferenceLow,
|
||||
sourceReferenceHigh: row.sourceReferenceHigh,
|
||||
sourceReferenceText: effectiveRange.sourceText,
|
||||
printedReferenceText: effectiveRange.printed?.text ?? null,
|
||||
referenceOrigin: effectiveRange.origin,
|
||||
referenceDivergesFromCatalog: effectiveRange.divergesFromCatalog,
|
||||
effectiveRange,
|
||||
@@ -206,6 +218,7 @@ export function serialiseLabResult(
|
||||
sourceReferenceLow: resolved.sourceReferenceLow,
|
||||
sourceReferenceHigh: resolved.sourceReferenceHigh,
|
||||
sourceReferenceText: resolved.sourceReferenceText,
|
||||
printedReferenceText: resolved.printedReferenceText,
|
||||
referenceOrigin: resolved.referenceOrigin,
|
||||
referenceDivergesFromCatalog: resolved.referenceDivergesFromCatalog,
|
||||
takenAt: row.takenAt.toISOString(),
|
||||
@@ -265,6 +278,7 @@ export function serialiseLabResultDetail(
|
||||
sourceReferenceLow: resolved.sourceReferenceLow,
|
||||
sourceReferenceHigh: resolved.sourceReferenceHigh,
|
||||
sourceReferenceText: resolved.sourceReferenceText,
|
||||
printedReferenceText: resolved.printedReferenceText,
|
||||
referenceOrigin: resolved.referenceOrigin,
|
||||
referenceDivergesFromCatalog: resolved.referenceDivergesFromCatalog,
|
||||
takenAt: row.takenAt.toISOString(),
|
||||
|
||||
Reference in new issue
Block a user