mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(daily): match a lead's metric by whole words
The terms that decide whether the Today lead already names the top signal's metric were bare stems: "risen" and "chosen" matched the Polish "sen" (sleep), "impulse" matched "puls", "Passing" matched the Italian "passi", and the Korean "잠" matched "잠시" (a moment). A lead about weight then dropped a sleep headline. Every term now has to start a word, whole-word terms have to end one (Unicode-aware on both sides), and the Korean terms carry their particle.
This commit is contained in:
1 parent
25c34b0e7c
commit
7400852fb4
2 files changed
+168
-18
No files matched your search
@@ -793,3 +793,74 @@ describe("signal line under the lead — nothing said twice", () => {
|
||||
expect(buildTodayOverview(input(), t).signalLine).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("signal line — a metric is named by whole words, not by fragments", () => {
|
||||
const DELTA = "↓ 50 min vs your usual";
|
||||
|
||||
function lineFor(
|
||||
lead: string,
|
||||
sourceMetric: NonNullable<
|
||||
DailyBriefing["signalsOfDay"]
|
||||
>[number]["sourceMetric"],
|
||||
) {
|
||||
return buildTodayOverview(
|
||||
input({
|
||||
briefing: {
|
||||
paragraph: lead,
|
||||
signalsOfDay: [
|
||||
{
|
||||
sourceMetric,
|
||||
tone: "info",
|
||||
headline: "A headline the lead does not quote.",
|
||||
nudge: "n",
|
||||
delta: DELTA,
|
||||
},
|
||||
],
|
||||
keyFindings: [],
|
||||
},
|
||||
}),
|
||||
t,
|
||||
).signalLine;
|
||||
}
|
||||
|
||||
// Each lead talks about something else; a fragment inside another word
|
||||
// used to count as naming the metric, and the headline was dropped.
|
||||
it.each([
|
||||
["Your weight has risen slightly this week.", "sleep"], // "sen"
|
||||
["You have chosen a steady routine.", "sleep"], // "sen"
|
||||
["Your readings worsen a little after lunch.", "sleep"], // "sen"
|
||||
["A snug fit for the cuff gave a clean reading.", "sleep"], // "snu"
|
||||
["Poranne ciśnienie spadło po spacerze.", "sleep"], // "spa" (pl)
|
||||
["잠시 후 다시 측정해 보세요.", "sleep"], // 잠시 = "a moment"
|
||||
["Passing showers kept the walk short.", "steps"], // "passi"
|
||||
["Ho passato una giornata tranquilla.", "steps"], // "passi" (it) inside passato
|
||||
["A sudden impulse to rest was a good call.", "pulse"], // "puls"
|
||||
["Der Impuls zur Pause war richtig.", "pulse"], // "puls" (de)
|
||||
["L'impulsione del giorno era calma.", "pulse"], // "puls" (it)
|
||||
["Wypadł impuls do odpoczynku.", "pulse"], // "puls" (pl)
|
||||
] as const)("keeps the headline under %j (%s)", (lead, metric) => {
|
||||
const line = lineFor(lead, metric);
|
||||
expect(line?.headline).toBe("A headline the lead does not quote.");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Last night you slept seven hours.", "sleep"],
|
||||
["Dein Schlaf war ruhig.", "sleep"],
|
||||
["Tu sueño fue tranquilo.", "sleep"],
|
||||
["Tu as bien dormi.", "sleep"],
|
||||
["Il sonno è stato regolare.", "sleep"],
|
||||
["Twój sen był spokojny.", "sleep"],
|
||||
["Mało snu tej nocy.", "sleep"],
|
||||
["Spałeś siedem godzin.", "sleep"],
|
||||
["어젯밤 잠을 잘 잤어요.", "sleep"],
|
||||
["수면 시간이 길었어요.", "sleep"],
|
||||
["Hai fatto molti passi oggi.", "steps"],
|
||||
["Your pulse settled overnight.", "pulse"],
|
||||
["Dein Puls war ruhig.", "pulse"],
|
||||
["Twój puls był spokojny.", "pulse"],
|
||||
["Il polso era regolare.", "pulse"],
|
||||
["Ton pouls était calme.", "pulse"],
|
||||
] as const)("still recognises %j as naming %s", (lead, metric) => {
|
||||
expect(lineFor(lead, metric)).toEqual({ headline: null, delta: DELTA });
|
||||
});
|
||||
});
|
||||
@@ -345,31 +345,110 @@ function restates(text: string, phrase: string): boolean {
|
||||
return p.length > 0 && normalise(text).includes(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* A term that must start a word: `stem` lets an inflection follow it
|
||||
* ("Blutdrucks", "tętna"), `word` must end where the word ends. Unicode-aware
|
||||
* on both sides, since `\b` only knows ASCII letters and would split "spał".
|
||||
* Without the boundaries a fragment inside another word named the metric:
|
||||
* "risen" read as the Polish "sen" (sleep), "impulse" as "puls".
|
||||
*/
|
||||
const START = "(?<![\\p{L}\\p{N}])";
|
||||
const END = "(?![\\p{L}\\p{N}])";
|
||||
const stem = (...terms: string[]) => terms.map((x) => `${START}${x}`);
|
||||
const word = (...terms: string[]) => terms.map((x) => `${START}${x}${END}`);
|
||||
/** Hangul has no word boundary to lean on: the term carries its particle. */
|
||||
const hangul = (...terms: string[]) => terms;
|
||||
const terms = (...parts: string[][]) =>
|
||||
new RegExp(parts.flat().join("|"), "iu");
|
||||
|
||||
const WEIGHT_TERMS = terms(
|
||||
stem("weigh", "gewicht"),
|
||||
word("peso", "pesos", "poids", "wag[aięe]", "wadze"),
|
||||
hangul("체중", "몸무게"),
|
||||
);
|
||||
|
||||
/**
|
||||
* How each briefing metric is named in prose, in every shipped language. The
|
||||
* lead is model text in the reader's language, so "does the lead already talk
|
||||
* about this metric" is a question about words. Stems, not whole words, so
|
||||
* inflected forms ("Blutdrucks", "tętna") still match. A metric missing here
|
||||
* simply falls back to the headline comparison.
|
||||
* about this metric" is a question about words, matched at word starts. A
|
||||
* metric missing here simply falls back to the headline comparison.
|
||||
*/
|
||||
const METRIC_TERMS: Partial<
|
||||
Record<DailyBriefingSignal["sourceMetric"], RegExp>
|
||||
> = {
|
||||
bp: /blood\s*pressure|blutdruck|presi[oó]n\s+arterial|tensi[oó]n\s+arterial|pression\s+art[ée]rielle|tension\s+art[ée]rielle|pressione\s+(arteriosa|sanguigna)|ci[sś]nieni|혈압/iu,
|
||||
weight: /weight|gewicht|peso|poids|wag[ai]|체중|몸무게/iu,
|
||||
glp1_plateau: /weight|gewicht|peso|poids|wag[ai]|체중|몸무게/iu,
|
||||
pulse: /pulse|puls|pouls|polso|t[eę]tn|맥박/iu,
|
||||
resting_hr:
|
||||
/resting\s+(heart\s*rate|pulse)|ruhepuls|ruhe-?herzfrequenz|en\s+reposo|au\s+repos|a\s+riposo|spoczynkow|안정\s*시?\s*심박/iu,
|
||||
hrv: /\bhrv\b|heart[-\s]*rate\s+variability|herzfrequenzvariabilit|variabilidad|variabilit[ée]|zmienno[sś][cć]|심박\s*변이/iu,
|
||||
sleep:
|
||||
/sleep|slept|schlaf|geschlafen|sue[nñ]o|dormi|sommeil|sonno|sen\b|snu|spa[lł]|수면|잠/iu,
|
||||
steps: /steps|schritte|pasos|passi|krok|걸음/iu,
|
||||
mood: /mood|stimmung|[aá]nimo|humeur|umore|nastr[oó]j|기분/iu,
|
||||
compliance:
|
||||
/medication|dose|medikament|einnahme|medicaci[oó]n|m[ée]dicament|farmac|lek[iów]|복약|약물/iu,
|
||||
body_temp: /temperature|temperatur|temperatura|temp[ée]rature|체온/iu,
|
||||
vo2_max: /vo2|vo₂/iu,
|
||||
bp: terms(
|
||||
stem(
|
||||
"blood\\s*pressure",
|
||||
"blutdruck",
|
||||
"presi[oó]n\\s+arterial",
|
||||
"tensi[oó]n\\s+arterial",
|
||||
"pression\\s+art[ée]rielle",
|
||||
"tension\\s+art[ée]rielle",
|
||||
"pressione\\s+(arteriosa|sanguigna)",
|
||||
"ci[sś]nieni",
|
||||
),
|
||||
hangul("혈압"),
|
||||
),
|
||||
weight: WEIGHT_TERMS,
|
||||
glp1_plateau: WEIGHT_TERMS,
|
||||
pulse: terms(
|
||||
word("pulses?", "puls(es)?", "pouls", "polso", "pulso"),
|
||||
stem("t[eę]tn"),
|
||||
hangul("맥박"),
|
||||
),
|
||||
resting_hr: terms(
|
||||
stem(
|
||||
"resting\\s+(heart\\s*rate|pulse)",
|
||||
"ruhepuls",
|
||||
"ruhe-?herzfrequenz",
|
||||
"en\\s+reposo",
|
||||
"au\\s+repos",
|
||||
"a\\s+riposo",
|
||||
"spoczynkow",
|
||||
),
|
||||
hangul("안정\\s*시?\\s*심박"),
|
||||
),
|
||||
hrv: terms(
|
||||
word("hrv"),
|
||||
stem(
|
||||
"heart[-\\s]*rate\\s+variability",
|
||||
"herzfrequenzvariabilit",
|
||||
"variabilidad\\s+de\\s+la\\s+frecuencia",
|
||||
"variabilit[ée]\\s+de\\s+la\\s+fr[ée]quence",
|
||||
"variabilit[àa]\\s+della\\s+frequenza",
|
||||
"zmienno[sś][cć]\\s+rytmu",
|
||||
),
|
||||
hangul("심박\\s*변이"),
|
||||
),
|
||||
sleep: terms(
|
||||
stem("sleep", "schlaf", "geschlafen", "dormi"),
|
||||
word(
|
||||
"slept",
|
||||
"sue[nñ]o",
|
||||
"sommeil",
|
||||
"sonno",
|
||||
"sen",
|
||||
"snu",
|
||||
"spa[lł](a|e[sś]|a[sś]|y)?",
|
||||
),
|
||||
hangul("수면", "잠을", "잠이", "잠은", "잠도"),
|
||||
),
|
||||
steps: terms(
|
||||
word("steps", "schritte", "schritten", "pasos", "passi", "krok(i|ów|ach)"),
|
||||
hangul("걸음"),
|
||||
),
|
||||
mood: terms(
|
||||
stem("stimmung"),
|
||||
word("mood", "[aá]nimo", "humeur", "umore", "nastr(ój|oj|oju)"),
|
||||
hangul("기분"),
|
||||
),
|
||||
compliance: terms(
|
||||
stem("medication", "medikament", "einnahme", "m[ée]dicament", "farmac"),
|
||||
word("doses?", "dosis", "medicaci[oó]n", "lek(i|ów|u|ami)?"),
|
||||
hangul("복약", "약물"),
|
||||
),
|
||||
body_temp: terms(stem("temperatur", "temp[ée]rature"), hangul("체온")),
|
||||
vo2_max: terms(stem("vo2", "vo₂")),
|
||||
};
|
||||
|
||||
/** Whether the lead already talks about the metric a signal is drawn from. */
|
||||
|
||||
Reference in new issue
Block a user