mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(gki): build the index from the glucose shown beside it
A reading entered in mmol/L is stored as whole mg/dL, and the index divided the unrounded conversion, so a 5.2 / 0.3 pair printed beside a 17.4 rather than 17.3. Round the glucose to the decimal it is shown with first and divide that. The tiles also printed a two-decimal ketone reading to one decimal; show it as recorded.
This commit is contained in:
1 parent
c8b32ccaea
commit
5ebfac1259
5 files changed
+72
-22
No files matched your search
@@ -4,7 +4,10 @@ import Link from "next/link";
|
||||
|
||||
import type { DayResponse } from "@/lib/day/contract";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { GKI_COMPONENT_UNIT } from "@/lib/measurements/glucose-ketone-index";
|
||||
import {
|
||||
GKI_COMPONENT_UNIT,
|
||||
ketoneFractionDigits,
|
||||
} from "@/lib/measurements/glucose-ketone-index";
|
||||
|
||||
type DayGki = NonNullable<DayResponse["glucoseKetoneIndex"]>[number];
|
||||
|
||||
@@ -38,7 +41,10 @@ export function DayGkiTile({ points }: { points: readonly DayGki[] }) {
|
||||
? tCount("day.gkiCount", points.length)
|
||||
: t("day.gkiPair", {
|
||||
glucose: fmt.number(latest.glucoseMmol, 1),
|
||||
ketones: fmt.number(latest.ketonesMmol, 1),
|
||||
ketones: fmt.number(
|
||||
latest.ketonesMmol,
|
||||
ketoneFractionDigits(latest.ketonesMmol),
|
||||
),
|
||||
unit: GKI_COMPONENT_UNIT,
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { GKI_COMPONENT_UNIT } from "@/lib/measurements/glucose-ketone-index";
|
||||
import {
|
||||
GKI_COMPONENT_UNIT,
|
||||
ketoneFractionDigits,
|
||||
} from "@/lib/measurements/glucose-ketone-index";
|
||||
import { useGlucoseKetoneIndexSeries } from "@/lib/queries/use-glucose-ketone-index";
|
||||
import { importWithRetry } from "@/lib/retry-import";
|
||||
|
||||
@@ -78,7 +81,10 @@ export function GkiCard() {
|
||||
{t("insights.bloodKetones.gki.latest", {
|
||||
value: fmt.number(latest.value, 1),
|
||||
glucose: fmt.number(latest.gki.glucoseMmol, 1),
|
||||
ketones: fmt.number(latest.gki.ketonesMmol, 1),
|
||||
ketones: fmt.number(
|
||||
latest.gki.ketonesMmol,
|
||||
ketoneFractionDigits(latest.gki.ketonesMmol),
|
||||
),
|
||||
unit: GKI_COMPONENT_UNIT,
|
||||
date: fmt.dateTime(latest.at),
|
||||
})}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MGDL_PER_MMOL } from "@/lib/glucose";
|
||||
import {
|
||||
GKI_PAIRING_WINDOW_MINUTES,
|
||||
glucoseKetoneIndex,
|
||||
ketoneFractionDigits,
|
||||
pairGlucoseKetoneIndex,
|
||||
summariseGlucoseKetoneIndex,
|
||||
type GkiReading,
|
||||
@@ -49,13 +50,25 @@ describe("pairGlucoseKetoneIndex", () => {
|
||||
glucoseId: "g1",
|
||||
at: new Date(T0).toISOString(),
|
||||
glucoseAt: new Date(T0).toISOString(),
|
||||
// 81 / 18.0182 = 4.4954 mmol/L; / 1.2 = 3.746 -> 3.7
|
||||
value: 3.7,
|
||||
// 81 / 18.0182 = 4.4954 -> 4.5 mmol/L, the figure shown beside the
|
||||
// index; 4.5 / 1.2 = 3.75 -> 3.8
|
||||
value: 3.8,
|
||||
ketonesMmol: 1.2,
|
||||
glucoseMmol: 4.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("builds the index from the glucose it prints, not the unrounded one", () => {
|
||||
// 5.2 mmol/L entered is stored as 94 mg/dL. Unrounded that is 5.217 and
|
||||
// 17.39 over 0.3, shown as 17.4 beside "5.2 / 0.3"; the pair gives 17.3.
|
||||
const [point] = pairGlucoseKetoneIndex(
|
||||
[reading("k1", 0, 0.3)],
|
||||
[reading("g1", 0, 94)],
|
||||
);
|
||||
expect(point?.glucoseMmol).toBe(5.2);
|
||||
expect(point?.value).toBe(17.3);
|
||||
});
|
||||
|
||||
it("prefers the same instant over a nearer-looking neighbour", () => {
|
||||
const [point] = pairGlucoseKetoneIndex(
|
||||
[reading("k1", 0, 1)],
|
||||
@@ -148,6 +161,16 @@ describe("pairGlucoseKetoneIndex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ketoneFractionDigits", () => {
|
||||
it("shows a ketone reading as recorded, one or two decimals", () => {
|
||||
expect(ketoneFractionDigits(1.2)).toBe(1);
|
||||
expect(ketoneFractionDigits(0.3)).toBe(1);
|
||||
expect(ketoneFractionDigits(2)).toBe(1);
|
||||
expect(ketoneFractionDigits(0.25)).toBe(2);
|
||||
expect(ketoneFractionDigits(1.05)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("summariseGlucoseKetoneIndex", () => {
|
||||
it("summarises the indices and is null for none", () => {
|
||||
expect(summariseGlucoseKetoneIndex([])).toBeNull();
|
||||
@@ -155,14 +178,14 @@ describe("summariseGlucoseKetoneIndex", () => {
|
||||
[reading("k1", 0, 1), reading("k2", 60, 2), reading("k3", 120, 4)],
|
||||
[reading("g1", 0, 90), reading("g2", 60, 90), reading("g3", 120, 90)],
|
||||
);
|
||||
// 90 mg/dL = 4.995 mmol/L: indices 5.0, 2.5, 1.2.
|
||||
expect(points.map((p) => p.value)).toEqual([5, 2.5, 1.2]);
|
||||
// 90 mg/dL = 4.995 -> 5.0 mmol/L: indices 5.0, 2.5, 1.3 (5.0 / 4 = 1.25).
|
||||
expect(points.map((p) => p.value)).toEqual([5, 2.5, 1.3]);
|
||||
expect(summariseGlucoseKetoneIndex(points)).toEqual({
|
||||
count: 3,
|
||||
mean: 2.9,
|
||||
min: 1.2,
|
||||
min: 1.3,
|
||||
max: 5,
|
||||
latest: 1.2,
|
||||
latest: 1.3,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,9 @@
|
||||
* GKI = glucose (mmol/L) / beta-hydroxybutyrate (mmol/L)
|
||||
*
|
||||
* Glucose is stored in mg/dL and converted with {@link MGDL_PER_MMOL}, the one
|
||||
* factor the tree uses for that conversion, so the glucose a GKI is built from
|
||||
* is the same number every glucose surface shows. The index is computed when
|
||||
* factor the tree uses for that conversion, then rounded to the one decimal
|
||||
* the index is shown beside, so the glucose a GKI is built from is the same
|
||||
* number every glucose surface shows. The index is computed when
|
||||
* it is read and never stored: a stored ratio drifts the moment either reading
|
||||
* it came from is edited or deleted, which is the failure a hand-typed GKI has.
|
||||
*
|
||||
@@ -50,6 +51,16 @@ export const GKI_PAIRING_WINDOW_MINUTES = 15;
|
||||
export const GKI_COMPONENT_UNIT = "mmol/L";
|
||||
export const GKI_PAIRING_WINDOW_MS = GKI_PAIRING_WINDOW_MINUTES * 60_000;
|
||||
|
||||
/**
|
||||
* The decimals a ketone reading is shown with beside its index: as recorded,
|
||||
* one or two. A meter that reads 0.25 must not print as 0.3, or the pair on
|
||||
* screen no longer divides to the index beside it.
|
||||
*/
|
||||
export function ketoneFractionDigits(ketonesMmol: number): 1 | 2 {
|
||||
const tenths = ketonesMmol * 10;
|
||||
return Math.abs(tenths - Math.round(tenths)) < 1e-9 ? 1 : 2;
|
||||
}
|
||||
|
||||
/** One stored reading, as the pairing needs it. */
|
||||
export interface GkiReading {
|
||||
id: string;
|
||||
@@ -154,16 +165,20 @@ export function pairGlucoseKetoneIndex(
|
||||
}
|
||||
}
|
||||
if (best === null) continue;
|
||||
const index = glucoseKetoneIndex(best.value, ketone.value);
|
||||
if (index === null) continue;
|
||||
if (glucoseKetoneIndex(best.value, ketone.value) === null) continue;
|
||||
// The index is built from the glucose as it is shown beside it, so the
|
||||
// printed pair divides to the printed index. A reading entered in mmol/L
|
||||
// is stored as whole mg/dL; dividing the unrounded conversion would land
|
||||
// a tenth away from the pair on screen.
|
||||
const glucoseMmol = oneDecimal(best.value / MGDL_PER_MMOL);
|
||||
out.push({
|
||||
ketoneId: ketone.id,
|
||||
glucoseId: best.id,
|
||||
at: ketone.measuredAt.toISOString(),
|
||||
glucoseAt: best.measuredAt.toISOString(),
|
||||
value: oneDecimal(index),
|
||||
value: oneDecimal(glucoseMmol / ketone.value),
|
||||
ketonesMmol: ketone.value,
|
||||
glucoseMmol: oneDecimal(best.value / MGDL_PER_MMOL),
|
||||
glucoseMmol,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -207,11 +207,11 @@ describe("glucose-ketone index (#1367)", () => {
|
||||
glucoseMmol: p.glucoseMmol,
|
||||
})),
|
||||
).toEqual([
|
||||
// 81 mg/dL = 4.5 mmol/L; / 1.2 = 3.7
|
||||
// 81 mg/dL = 4.5 mmol/L; / 1.2 = 3.75 -> 3.8
|
||||
{
|
||||
at: at(0).toISOString(),
|
||||
glucoseAt: at(0).toISOString(),
|
||||
value: 3.7,
|
||||
value: 3.8,
|
||||
ketonesMmol: 1.2,
|
||||
glucoseMmol: 4.5,
|
||||
},
|
||||
@@ -261,7 +261,7 @@ describe("glucose-ketone index (#1367)", () => {
|
||||
};
|
||||
};
|
||||
expect(gkiBody.data.unit).toBe("ratio");
|
||||
expect(gkiBody.data.points.map((p) => p.value)).toEqual([3.7, 2.5]);
|
||||
expect(gkiBody.data.points.map((p) => p.value)).toEqual([3.8, 2.5]);
|
||||
expect(gkiBody.data.points[1]!.gki).toMatchObject({
|
||||
glucoseAt: at(130).toISOString(),
|
||||
glucoseMmol: 5,
|
||||
@@ -301,7 +301,7 @@ describe("glucose-ketone index (#1367)", () => {
|
||||
};
|
||||
};
|
||||
expect(body.data.glucoseKetoneIndex?.map((p) => p.value)).toEqual([
|
||||
3.7, 2.5,
|
||||
3.8, 2.5,
|
||||
]);
|
||||
expect(body.data.values.some((v) => v.type === "BLOOD_KETONES")).toBe(true);
|
||||
});
|
||||
@@ -324,8 +324,8 @@ describe("glucose-ketone index (#1367)", () => {
|
||||
count: 2,
|
||||
latest: 2.5,
|
||||
min: 2.5,
|
||||
max: 3.7,
|
||||
avg: 3.1,
|
||||
max: 3.8,
|
||||
avg: 3.2,
|
||||
});
|
||||
|
||||
const ketonesOnly = await collectDoctorReportData(
|
||||
|
||||
Reference in new issue
Block a user