Files
HealthLog/src/lib/measurements/entry-units.ts
T
Marc-André Bombeck 7669868479 refactor(measurements): drop the entry-unit helpers nothing calls
`hasEntryUnitConversion` and `canonicalValueToEntry` shipped beside the
hours-to-minutes conversion for an editing surface that does not exist and has
no ticket. Neither has a caller, so both were only two more things to keep
true, and the dead-code gate is right to say so.

The module comment claimed the inverse was there so a future surface could not
land on a different answer. It says what is actually true now: one direction,
one place. The git history keeps the rest if that surface ever arrives.
2026-08-07 18:32:38 +02:00

68 lines
3.0 KiB
TypeScript

/**
* Where the entry form asks for one unit and the column stores another.
*
* This is not the metric/imperial display transform in `display-transform.ts`:
* that one follows a preference and is the identity for anyone on metric. This
* one holds regardless of preference, because the storage unit was chosen for
* the sync sources rather than for the person typing.
*
* Sleep is the case. `SLEEP_DURATION` is stored in minutes — every provider
* delivers minutes, and a per-stage segment is only a few of them — while a
* person logging a night by hand thinks in hours and the field says so. The
* form sent the typed number straight through, so a night entered as 7.5 was
* stored as seven and a half MINUTES and read back on the list as eight
* minutes of sleep. Nothing rejected it: seven and a half minutes is inside
* the column's plausibility band, because a single sleep STAGE really can be
* that short.
*
* The storage unit stays minutes. The conversion belongs at the entry
* boundary, in one place, so a second surface that starts taking sleep by hand
* has one answer to reach for rather than its own.
*
* Only the direction that exists is here. An inverse and a "does this type
* convert" predicate shipped alongside it at first, written for an editing
* surface that does not exist and has no ticket; they were nothing but two
* more things to keep true. The git history holds them if that surface ever
* arrives.
*/
export const MINUTES_PER_HOUR = 60;
/**
* Entry unit → canonical unit multiplier, per measurement type. A type absent
* from this table enters in the unit it is stored in.
*/
const ENTRY_TO_CANONICAL_FACTOR: Readonly<Record<string, number>> = {
SLEEP_DURATION: MINUTES_PER_HOUR,
};
/**
* Convert a value as typed in the entry form to the unit the column stores.
*
* The result is rounded to the nearest whole canonical unit for a converted
* type: 7.3 hours is 438 minutes, and floating-point multiplication would
* otherwise store 437.99999999999994 for it. An unconverted type is returned
* untouched, so no existing metric changes shape.
*/
export function entryValueToCanonical(type: string, entered: number): number {
const factor = ENTRY_TO_CANONICAL_FACTOR[type];
if (factor === undefined || !Number.isFinite(entered)) return entered;
return Math.round(entered * factor);
}
/**
* Read a number out of an entry field, accepting the decimal comma.
*
* Most of the application's readers are German, and "7,5" is how they write
* seven and a half. `parseFloat` reads that as 7 and discards the rest without
* complaint — the worst shape of wrong, since the entry looks accepted.
* Returns `null` for anything that is not a number, so the caller decides what
* an empty field means rather than inheriting a `NaN`.
*/
export function parseDecimalEntry(raw: string): number | null {
const trimmed = raw.trim().replace(",", ".");
if (trimmed === "") return null;
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : null;
}