Files
HealthLog/src/lib/glucose.ts
T
Marc-André Bombeck 170862fdf8 fix(targets): convert glucose targets back to mg/dL on save
The per-metric target editor receives a glucose range already projected
into the user's display unit (mmol/L) along with `unit="mmol/L"`, but it
seeded, validated and saved that value verbatim. HealthLog stores glucose
canonically in mg/dL, so a user on the mmol/L preference either had a
valid target (e.g. 3.9–5.5 mmol/L) rejected by the 40–400 mg/dL bounds,
or — when an override already existed — saw a mg/dL number rendered as if
it were mmol/L and wrote it straight back.

Add `mmolToMgdl` / `toCanonicalMgdl` (the inverse of `convertGlucose`) and
teach the edit sheet to treat the display unit at its boundary: seed the
persisted mg/dL override into the display unit, validate against bounds
expressed in the display unit, and convert the typed value back to
canonical mg/dL before the PUT. A no-op for every non-glucose metric and
for users on the mg/dL preference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0cc0361058a85f32d7135e5f2fd425c98f62d885)
2026-06-02 17:01:14 +02:00

65 lines
2.0 KiB
TypeScript

/**
* Blood-glucose unit conversions.
*
* HealthLog stores glucose canonically in **mg/dL**. The UI can display in
* mmol/L for users who prefer that (SI-unit countries). Conversion factor is
* 18.0182 (per DGIM / DDG S3 guideline). We round to clinically meaningful
* precision: 0 fractional digits for mg/dL, 1 for mmol/L.
*/
export type GlucoseUnit = "mg/dL" | "mmol/L";
const MGDL_PER_MMOL = 18.0182;
export function mgdlToMmol(mgdl: number): number {
return Math.round((mgdl / MGDL_PER_MMOL) * 10) / 10;
}
export function mmolToMgdl(mmol: number): number {
return Math.round(mmol * MGDL_PER_MMOL);
}
export function convertGlucose(value: number, to: GlucoseUnit): number {
return to === "mmol/L" ? mgdlToMmol(value) : Math.round(value);
}
/**
* Convert a value the user typed in their display unit back to the
* canonical mg/dL HealthLog stores. The inverse of {@link convertGlucose}
* for the editor write path: a `5.5 mmol/L` target the user enters must
* persist as `99 mg/dL`, not as the literal `5.5`. A `mg/dL` display unit
* is already canonical, so it only rounds.
*/
export function toCanonicalMgdl(value: number, from: GlucoseUnit): number {
return from === "mmol/L" ? mmolToMgdl(value) : Math.round(value);
}
export function resolveGlucoseUnit(
userPreference: string | null | undefined,
): GlucoseUnit {
return userPreference === "mmol/L" ? "mmol/L" : "mg/dL";
}
/**
* Threshold metric key corresponding to a glucose context. Keeps the
* effective-range resolver the single source of truth.
*/
export function thresholdMetricForContext(
context: "FASTING" | "POSTPRANDIAL" | "RANDOM" | "BEDTIME",
):
| "BLOOD_GLUCOSE_FASTING"
| "BLOOD_GLUCOSE_POSTPRANDIAL"
| "BLOOD_GLUCOSE_RANDOM"
| "BLOOD_GLUCOSE_BEDTIME" {
switch (context) {
case "FASTING":
return "BLOOD_GLUCOSE_FASTING";
case "POSTPRANDIAL":
return "BLOOD_GLUCOSE_POSTPRANDIAL";
case "RANDOM":
return "BLOOD_GLUCOSE_RANDOM";
case "BEDTIME":
return "BLOOD_GLUCOSE_BEDTIME";
}
}