mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(ai): let the accented-word safety and grounding patterns match
JavaScript's \b is an ASCII word boundary, so next to an accented letter it never matches. That left patterns, or alternatives of them, dead: the French "en bonne santé", "à cause de", "causé" and "continuez à", "probabilité" and "probabilità" in the French and Italian risk banks, the Dutch "één procent", the German "Ärzte", and the Polish "przedział", "średnia" and the verb endings in "-ą". A \w* stem cannot run through "ł" either, so "doprowadziło do" missed its causal pattern, and the Polish adult pattern was spelled "dorosli" without the "ś". Those boundaries now use lookarounds that count the Latin letters with diacritics as word letters (WORD_START / WORD_END in the shared screen module, the same class inline in the regex literals), the Polish stems take accented letters, and "dorośli" is spelled both ways. Two guards keep the class out: every pattern of every locale's screen and population-norm bank has to match at least one realistic sentence, so a dead pattern fails the suite, and no pattern may put \b next to an accented letter. Both fail on the old patterns.
This commit is contained in:
1 parent
b89cb6a29f
commit
abb79ee6ae
11 files changed
+701
-40
No files matched your search
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Find `\b` word boundaries that sit next to an accented letter in a regular
|
||||
* expression's source.
|
||||
*
|
||||
* JavaScript defines `\b` over ASCII `\w`, so `santé\b` never matches before a
|
||||
* space and `\bépisode` never matches after one; an alternative that starts or
|
||||
* ends that way is dead. The check looks at what the source puts on each side
|
||||
* of the boundary: a literal letter, a character class, or a group whose
|
||||
* alternatives start (or end) with one. It is a text heuristic over the
|
||||
* pattern source, not a regex engine: a boundary next to a backreference or a
|
||||
* lookaround it does not unwrap is not judged.
|
||||
*/
|
||||
function isAccentedLetter(char: string | undefined): boolean {
|
||||
return char !== undefined && char.charCodeAt(0) > 127 && /\p{L}/u.test(char);
|
||||
}
|
||||
|
||||
function classEnd(source: string, open: number): number {
|
||||
let i = open + 1;
|
||||
while (i < source.length && !(source[i] === "]" && source[i - 1] !== "\\")) {
|
||||
i += 1;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function classHasAccent(source: string, open: number): boolean {
|
||||
return [...source.slice(open, classEnd(source, open))].some(isAccentedLetter);
|
||||
}
|
||||
|
||||
/** Top-level alternatives of the group opening at `open`, and its close. */
|
||||
function alternatives(source: string, open: number): string[] {
|
||||
const prefix = /^\(\?(?:<[=!]|[:=!])/.exec(source.slice(open))?.[0] ?? "(";
|
||||
const alts: string[] = [];
|
||||
let depth = 0;
|
||||
let start = open + prefix.length;
|
||||
for (let i = open; i < source.length; i += 1) {
|
||||
const c = source[i];
|
||||
if (c === "\\") {
|
||||
i += 1;
|
||||
} else if (c === "[") {
|
||||
i = classEnd(source, i);
|
||||
} else if (c === "(") {
|
||||
depth += 1;
|
||||
} else if (c === ")") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
alts.push(source.slice(start, i));
|
||||
return alts;
|
||||
}
|
||||
} else if (c === "|" && depth === 1) {
|
||||
alts.push(source.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return alts;
|
||||
}
|
||||
|
||||
function opensWithAccent(text: string): boolean {
|
||||
if (text.startsWith("[")) return classHasAccent(text, 0);
|
||||
if (text.startsWith("(")) return alternatives(text, 0).some(opensWithAccent);
|
||||
return isAccentedLetter(text[0]);
|
||||
}
|
||||
|
||||
function closesWithAccent(text: string): boolean {
|
||||
const trimmed = text.replace(/(?:[?*+]|\{\d+(?:,\d*)?\})+\??$/, "");
|
||||
const last = trimmed[trimmed.length - 1];
|
||||
if (last === "]") {
|
||||
let open = trimmed.length - 2;
|
||||
while (
|
||||
open >= 0 &&
|
||||
!(trimmed[open] === "[" && trimmed[open - 1] !== "\\")
|
||||
) {
|
||||
open -= 1;
|
||||
}
|
||||
return classHasAccent(trimmed, open);
|
||||
}
|
||||
if (last === ")") {
|
||||
let depth = 0;
|
||||
for (let i = trimmed.length - 1; i >= 0; i -= 1) {
|
||||
if (trimmed[i - 1] === "\\") continue;
|
||||
if (trimmed[i] === ")") depth += 1;
|
||||
else if (trimmed[i] === "(" && --depth === 0) {
|
||||
return alternatives(trimmed, i).some(closesWithAccent);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return isAccentedLetter(last);
|
||||
}
|
||||
|
||||
/** Offsets of every `\b` in `source` that touches an accented letter. */
|
||||
export function accentedBoundaries(source: string): number[] {
|
||||
const hits: number[] = [];
|
||||
for (
|
||||
let i = source.indexOf("\\b");
|
||||
i >= 0;
|
||||
i = source.indexOf("\\b", i + 2)
|
||||
) {
|
||||
if (source[i - 1] === "\\") continue;
|
||||
if (
|
||||
opensWithAccent(source.slice(i + 2)) ||
|
||||
closesWithAccent(source.slice(0, i))
|
||||
) {
|
||||
hits.push(i);
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Every population-norm pattern of every locale has to match at least one
|
||||
* realistic reference sentence below. A pattern that never fires exempts
|
||||
* nothing and fails silently; French "en bonne santé" and Polish "zdrowi
|
||||
* dorośli" were dead that way, the one behind an ASCII `\b` after "é", the
|
||||
* other spelled for "dorosli" without the "ś".
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { accentedBoundaries } from "@/__tests__/helpers/ascii-boundary";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
import { POPULATION_NORM_PATTERNS } from "..";
|
||||
|
||||
const SENTENCES: Record<Locale, readonly string[]> = {
|
||||
en: [
|
||||
"The normal range is 60 to 100.",
|
||||
"Healthy adults sleep 7 to 9 hours.",
|
||||
"The general population averages 120.",
|
||||
"Guidelines recommend 150 minutes a week.",
|
||||
"Adults generally need 7 hours of sleep.",
|
||||
"The reference range is 4 to 6.",
|
||||
"The recommended amount is 2 litres.",
|
||||
],
|
||||
de: [
|
||||
"Der Normalbereich liegt bei 60 bis 100.",
|
||||
"Die normale Herzfrequenz liegt bei 60.",
|
||||
"Gesunde Erwachsene schlafen 8 Stunden.",
|
||||
"Ärzte empfehlen 150 Minuten pro Woche.",
|
||||
"Erwachsene brauchen in der Regel 8 Stunden.",
|
||||
"Normale Werte liegen bei 4 bis 6.",
|
||||
"Der Puls liegt normalerweise zwischen 60 und 100.",
|
||||
],
|
||||
fr: [
|
||||
"La plage normale va de 60 à 100.",
|
||||
"Les adultes en bonne santé dorment 8 heures.",
|
||||
"La plupart des adultes dorment 8 heures.",
|
||||
"Les médecins recommandent 150 minutes.",
|
||||
"Les adultes ont besoin de 8 heures.",
|
||||
"Elle se situe généralement entre 60 et 100.",
|
||||
],
|
||||
es: [
|
||||
"El rango normal va de 60 a 100.",
|
||||
"Los adultos sanos duermen 8 horas.",
|
||||
"La mayoría de los adultos duerme 8 horas.",
|
||||
"Los médicos recomiendan 150 minutos.",
|
||||
"Los adultos generalmente necesitan 8 horas.",
|
||||
"Se sitúa normalmente entre 60 y 100.",
|
||||
],
|
||||
it: [
|
||||
"L'intervallo normale va da 60 a 100.",
|
||||
"Gli adulti sani dormono 8 ore.",
|
||||
"La maggior parte degli adulti dorme 8 ore.",
|
||||
"I medici raccomandano 150 minuti.",
|
||||
"Gli adulti di solito hanno bisogno di 8 ore.",
|
||||
"Si colloca di solito tra 60 e 100.",
|
||||
],
|
||||
pl: [
|
||||
"Prawidłowy zakres wynosi 60 do 100.",
|
||||
"Zdrowi dorośli śpią 8 godzin.",
|
||||
"Większość dorosłych śpi 8 godzin.",
|
||||
"Lekarze zalecają 150 minut.",
|
||||
"Dorośli zwykle potrzebują 8 godzin.",
|
||||
"Zakres referencyjny to 4 do 6.",
|
||||
"Wynosi zwykle od 60 do 100.",
|
||||
],
|
||||
nl: [
|
||||
"Het normale bereik is 60 tot 100.",
|
||||
"Een normale hartslag ligt rond 60.",
|
||||
"De meeste volwassenen slapen 8 uur.",
|
||||
"De algemene bevolking slaapt 7 uur.",
|
||||
"Artsen adviseren 150 minuten per week.",
|
||||
"Volwassenen hebben doorgaans nodig: 7 tot 9 uur slaap.",
|
||||
"Het referentiebereik is 4 tot 6.",
|
||||
"Je hartslag ligt meestal tussen 60 en 100.",
|
||||
],
|
||||
ko: [
|
||||
"정상 범위는 60~100이에요.",
|
||||
"참고 범위는 4~6이에요.",
|
||||
"건강한 성인은 8시간 자요.",
|
||||
"일반 인구는 7시간 자요.",
|
||||
"전문가들은 150분을 권장해요.",
|
||||
"성인은 보통 8시간이 필요해요.",
|
||||
"보통 60에서 100 사이예요.",
|
||||
],
|
||||
"pt-BR": [
|
||||
"A faixa normal vai de 60 a 100.",
|
||||
"Os valores de referência vão de 4 a 6.",
|
||||
"Adultos saudáveis dormem 8 horas.",
|
||||
"A maioria dos adultos dorme 8 horas.",
|
||||
"Os médicos recomendam 150 minutos por semana.",
|
||||
"Os adultos precisam de 8 horas de sono.",
|
||||
"A pressão arterial normal fica abaixo de 120/80.",
|
||||
],
|
||||
};
|
||||
|
||||
describe("population-norm banks: no pattern is dead", () => {
|
||||
it.each(locales)(
|
||||
"every %s pattern matches one of its sentences",
|
||||
(locale) => {
|
||||
const dead = POPULATION_NORM_PATTERNS[locale]
|
||||
.map((pattern, index): [number, RegExp] => [index, pattern])
|
||||
.filter(
|
||||
([, pattern]) => !SENTENCES[locale].some((s) => pattern.test(s)),
|
||||
)
|
||||
.map(([index, pattern]) => `[${index}] /${pattern.source}/`);
|
||||
expect(dead, `${locale} patterns no sentence matches`).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(locales)(
|
||||
"no %s pattern puts an ASCII word boundary next to an accented letter",
|
||||
(locale) => {
|
||||
const offenders = POPULATION_NORM_PATTERNS[locale]
|
||||
.map((pattern, index): [string, RegExp] => [`[${index}]`, pattern])
|
||||
.filter(([, pattern]) => accentedBoundaries(pattern.source).length > 0)
|
||||
.map(([name]) => name);
|
||||
expect(offenders, `${locale} population-norm patterns`).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ export const patterns: readonly RegExp[] = [
|
||||
/\bdie\s+normale\b/i,
|
||||
/\bnormal(?:er|e)?\s+(?:ruhe)?(?:herzfrequenz|puls|bereich|wert)\b/i,
|
||||
/\b(?:gesunde|die\s+meisten|der\s+durchschnittliche?)\s+(?:erwachsene|menschen|bevölkerung)\b/i,
|
||||
/\b(?:leitlinien|ärzte|expert\w*)\s+(?:sagen|empfehlen|raten|betrachten)\b/i,
|
||||
/(?<![\w\u00C0-\u024F])(?:leitlinien|ärzte|expert\w*)\s+(?:sagen|empfehlen|raten|betrachten)\b/i,
|
||||
/\b(?:erwachsene|menschen)\s+(?:brauchen|benötigen)\s+(?:in\s+der\s+regel|generell|typischerweise|normalerweise)\b/i,
|
||||
/\b(?:allgemeine|normale|typische)\s+(?:referenz)?(?:bereich|spanne|werte)\b/i,
|
||||
/\bliegt\s+(?:normalerweise|üblicherweise|in\s+der\s+regel)\s+(?:bei|zwischen)\b/i,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Population-norm framing, fr. */
|
||||
export const patterns: readonly RegExp[] = [
|
||||
/\bla\s+(?:plage|fourchette)\s+normale\b/i,
|
||||
/\b(?:les\s+)?(?:adultes|personnes)\s+en\s+bonne\s+santé\b/i,
|
||||
/\b(?:les\s+)?(?:adultes|personnes)\s+en\s+bonne\s+santé(?![\w\u00C0-\u024F])/i,
|
||||
/\b(?:la\s+plupart\s+des|la\s+moyenne\s+des)\s+(?:adultes|gens|personnes)\b/i,
|
||||
/\b(?:les\s+)?(?:recommandations|médecins|experts?)\s+(?:disent|recommandent|conseillent|suggèrent)\b/i,
|
||||
/\b(?:les\s+)?adultes\s+(?:ont\s+généralement|ont\s+besoin|nécessitent)\b/i,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Population-norm framing, pl. */
|
||||
export const patterns: readonly RegExp[] = [
|
||||
/\b(?:normalny|prawidłowy)\s+(?:zakres|przedział)\b/i,
|
||||
/\bzdrow(?:i|ych)\s+doros(?:li|łych)\b/i,
|
||||
/\b(?:większość|średnia)\s+(?:doros(?:łych|li)|ludzi|osób)\b/i,
|
||||
/\b(?:wytyczne|lekarze|eksperci)\s+(?:mówią|zalecają|radzą|sugerują)\b/i,
|
||||
/\bdoros(?:li|łych)\s+(?:zazwyczaj|zwykle|na\s+ogół)\s+(?:potrzebują|wymagają)\b/i,
|
||||
/\b(?:normalny|prawidłowy)\s+(?:zakres|przedział)(?![\w\u00C0-\u024F])/i,
|
||||
/\bzdrow(?:i|ych)\s+doro(?:[sś]li|słych)\b/i,
|
||||
/(?<![\w\u00C0-\u024F])(?:większość|średnia)\s+(?:doro(?:słych|[sś]li)|ludzi|osób)\b/i,
|
||||
/\b(?:wytyczne|lekarze|eksperci)\s+(?:mówią|zalecają|radzą|sugerują)(?![\w\u00C0-\u024F])/i,
|
||||
/\bdoro(?:[sś]li|słych)\s+(?:zazwyczaj|zwykle|na\s+ogół)\s+(?:potrzebują|wymagają)(?![\w\u00C0-\u024F])/i,
|
||||
/\b(?:zakres|przedział)\s+(?:referencyjny|normy|prawidłowy|zalecany)\b/i,
|
||||
/\b(?:wynosi|mieści\s+się)\s+(?:zazwyczaj|zwykle|na\s+ogół)\s+(?:od|między|około)\b/i,
|
||||
];
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Every pattern of every locale's outbound-screen bank has to match at least
|
||||
* one realistic sentence below.
|
||||
*
|
||||
* A pattern that can never fire is a contract the screen only appears to
|
||||
* enforce, and nothing else notices: the bank stays green because the other
|
||||
* patterns carry the tests. JavaScript's `\b` is an ASCII boundary, so next to
|
||||
* an accented letter it never matches: French "à cause de", "causé" and
|
||||
* "continuez à", the French and Italian "probabilité" / "probabilità" and the
|
||||
* Dutch "één procent" all walked past their patterns, and a `\w*` stem cannot
|
||||
* run through "ł", so Polish "doprowadziło do" did too. A new pattern needs a
|
||||
* sentence here; a sentence that stops matching after an edit points at the
|
||||
* pattern it broke.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { accentedBoundaries } from "@/__tests__/helpers/ascii-boundary";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
import { bank as en } from "../outbound-screen/locales/en";
|
||||
import { bank as de } from "../outbound-screen/locales/de";
|
||||
import { bank as fr } from "../outbound-screen/locales/fr";
|
||||
import { bank as es } from "../outbound-screen/locales/es";
|
||||
import { bank as it_ } from "../outbound-screen/locales/it";
|
||||
import { bank as pl } from "../outbound-screen/locales/pl";
|
||||
import { bank as nl } from "../outbound-screen/locales/nl";
|
||||
import { bank as ko } from "../outbound-screen/locales/ko";
|
||||
import { bank as ptBR } from "../outbound-screen/locales/pt-br";
|
||||
import type { LocaleBank } from "../outbound-screen/shared";
|
||||
|
||||
const BANKS: Record<Locale, LocaleBank> = {
|
||||
en,
|
||||
de,
|
||||
fr,
|
||||
es,
|
||||
it: it_,
|
||||
pl,
|
||||
nl,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
};
|
||||
|
||||
const SENTENCES: Record<Locale, readonly string[]> = {
|
||||
en: [
|
||||
"Increase your metformin to 2000 mg daily.",
|
||||
"You might want to consider increasing your insulin to 20 units.",
|
||||
"Take 2000 mg daily.",
|
||||
"Skip your evening dose.",
|
||||
"Stop taking metformin.",
|
||||
"I'd recommend skipping your evening dose.",
|
||||
"Take an extra ramipril.",
|
||||
"Stop your blood thinner before the run.",
|
||||
"Leave your metformin out tomorrow.",
|
||||
"I'd recommend stopping your blood thinner.",
|
||||
"Step up to 2.4 mg next week.",
|
||||
"Lower it to 5 mg.",
|
||||
"You should try 5 mg.",
|
||||
"Skip your dose for two weeks to see.",
|
||||
"That is a 12% risk.",
|
||||
"Your risk is about 12%.",
|
||||
"Your risk is roughly twelve percent.",
|
||||
"Twelve percent is your risk.",
|
||||
"Your 10-year cardiovascular risk is about 12%.",
|
||||
"About 12% is your 10-year cardiovascular risk.",
|
||||
"Your ASCVD score comes out at 14%.",
|
||||
"A 14% figure is what ASCVD gives.",
|
||||
"SCORE2 would put you in a higher bracket.",
|
||||
"Your SCORE2 lands in the high-risk band.",
|
||||
"Your 10-year cardiovascular risk is elevated.",
|
||||
"Your weight fell because you slept more.",
|
||||
"It was caused by stress.",
|
||||
"That is due to salt.",
|
||||
"It led to a dip.",
|
||||
"It resulted in a dip.",
|
||||
"It is the result of travel.",
|
||||
"Sleep is the culprit.",
|
||||
"It was driven by stress.",
|
||||
"Thanks to the walks, it fell.",
|
||||
"Owing to the heat, it rose.",
|
||||
"Salt is responsible for it.",
|
||||
"Keep taking your 7.5 mg as prescribed.",
|
||||
],
|
||||
de: [
|
||||
"Erhöhe dein Metformin auf 2000 mg.",
|
||||
"Nimm 2 Tabletten am Abend.",
|
||||
"Pausiere dein Insulin.",
|
||||
"Setz das Metformin ab.",
|
||||
"Lass die Abenddosis weg.",
|
||||
"Hör auf, Metformin zu nehmen.",
|
||||
"Du solltest das Metformin absetzen.",
|
||||
"Nimm abends eine Tablette mehr.",
|
||||
"Hör mit dem Insulin auf.",
|
||||
"Verzichte auf das Insulin.",
|
||||
"Erhöhe auf 7,5 mg.",
|
||||
"Reduziere auf 5 mg.",
|
||||
"Die nächste Stufe sind 10 mg.",
|
||||
"Es ist sinnvoll, auf 7,5 mg zu erhöhen.",
|
||||
"Ich empfehle 5 mg.",
|
||||
"Halbiere die Dosis für zwei Wochen.",
|
||||
"Dein Risiko liegt bei etwa 14%.",
|
||||
"Das sind 14% Risiko.",
|
||||
"Dein Risiko liegt bei vierzehn Prozent.",
|
||||
"Vierzehn Prozent Risiko bleiben.",
|
||||
"Dein 10-Jahres-Risiko liegt bei 14%.",
|
||||
"14% beträgt dein 10-Jahres-Risiko.",
|
||||
"Der SCORE2 ergibt 14%.",
|
||||
"Mit 14% laut SCORE2.",
|
||||
"SCORE2 stuft dich ein als hoch.",
|
||||
"SCORE2 zeigt einen hohen Risikobereich.",
|
||||
"Dein 10-Jahres-Risiko ist erhöht.",
|
||||
"Das Gewicht sank, weil du mehr geschlafen hast.",
|
||||
"Es sank wegen des Schlafs.",
|
||||
"Das verursacht Stress.",
|
||||
"Es stieg aufgrund der Hitze.",
|
||||
"Das führte zu einem Anstieg.",
|
||||
"Er wurde durch Stress verursacht.",
|
||||
"Schuld ist das Salz.",
|
||||
"Der Auslöser war Salz.",
|
||||
"Salz ist verantwortlich für den Anstieg.",
|
||||
"Nimm dein Metformin weiter wie verordnet.",
|
||||
],
|
||||
fr: [
|
||||
"Augmentez votre metformine à 2000 mg.",
|
||||
"Prenez 2 comprimés le soir.",
|
||||
"Sautez votre dose du soir.",
|
||||
"Arrêtez de prendre la metformine.",
|
||||
"Je recommande de suspendre votre insuline.",
|
||||
"Prenez un comprimé en plus.",
|
||||
"Augmentez à 2,4 mg.",
|
||||
"Réduisez à 5 mg.",
|
||||
"Je recommande 5 mg.",
|
||||
"La prochaine dose sera de 10 mg.",
|
||||
"Votre risque est d'environ 12%.",
|
||||
"Il y a 12% de risque.",
|
||||
"Votre risque cardiovasculaire à 10 ans.",
|
||||
"Votre risque est de douze pour cent.",
|
||||
"Douze pour cent de risque.",
|
||||
"SCORE2 donne 12%.",
|
||||
"12% selon SCORE2.",
|
||||
"SCORE2 vous place dans la tranche haute.",
|
||||
"Votre poids a baissé parce que vous avez mieux dormi.",
|
||||
"Il a monté à cause de la chaleur.",
|
||||
"Le sel cause cela.",
|
||||
"Il a monté en raison de la chaleur.",
|
||||
"Cela entraîne une hausse.",
|
||||
"Le sommeil a conduit à une baisse.",
|
||||
"C'est dû à la chaleur.",
|
||||
"Le sel est responsable de la hausse.",
|
||||
"Il a baissé grâce à la marche.",
|
||||
"Le stress provoque une hausse.",
|
||||
"Continuez à prendre votre metformine.",
|
||||
],
|
||||
es: [
|
||||
"Aumente su metformina a 2000 mg.",
|
||||
"Tome 2 comprimidos por la noche.",
|
||||
"Omita la dosis de la noche.",
|
||||
"Deje de tomar la metformina.",
|
||||
"Tome una pastilla de más.",
|
||||
"Deje el anticoagulante.",
|
||||
"Aumente a 2,4 mg.",
|
||||
"Reduzca a 5 mg.",
|
||||
"Recomiendo 5 mg.",
|
||||
"La próxima dosis será de 10 mg.",
|
||||
"Su riesgo es del 12%.",
|
||||
"Hay un 12% de riesgo.",
|
||||
"Su riesgo cardiovascular a 10 años.",
|
||||
"Su riesgo es de doce por ciento.",
|
||||
"Doce por ciento de riesgo.",
|
||||
"SCORE2 da 12%.",
|
||||
"12% según SCORE2.",
|
||||
"SCORE2 lo coloca en la franja alta.",
|
||||
"Su peso bajó porque durmió más.",
|
||||
"Subió debido a la sal.",
|
||||
"Subió a causa de la sal.",
|
||||
"La sal causa eso.",
|
||||
"La sal provoca una subida.",
|
||||
"Eso llevó a una bajada.",
|
||||
"La sal es responsable de la subida.",
|
||||
"Bajó gracias a la caminata.",
|
||||
"Fue impulsado por el estrés.",
|
||||
"Siga tomando su metformina.",
|
||||
],
|
||||
it: [
|
||||
"Aumenti la metformina a 2000 mg.",
|
||||
"Prenda 2 compresse la sera.",
|
||||
"Salti la dose serale.",
|
||||
"Smetta di prendere la metformina.",
|
||||
"Prenda una compressa in più.",
|
||||
"Aumenti a 2,4 mg.",
|
||||
"Riduca a 5 mg.",
|
||||
"Raccomando 5 mg.",
|
||||
"La prossima dose sarà di 10 mg.",
|
||||
"Il suo rischio è del 12%.",
|
||||
"C'è il 12% di rischio.",
|
||||
"Il suo rischio cardiovascolare a 10 anni.",
|
||||
"Il rischio è del dodici per cento.",
|
||||
"Dodici per cento di rischio.",
|
||||
"SCORE2 dà 12%.",
|
||||
"12% secondo SCORE2.",
|
||||
"SCORE2 la colloca nella fascia alta.",
|
||||
"Il suo peso è calato perché ha dormito di più.",
|
||||
"È salito a causa di troppo sale.",
|
||||
"Il sale causa questo.",
|
||||
"Il sale provoca un aumento.",
|
||||
"Il sonno ha portato a un calo.",
|
||||
"È dovuto a stress.",
|
||||
"Il sale è responsabile di un aumento.",
|
||||
"È calato grazie a una camminata.",
|
||||
"È stato guidato da stress.",
|
||||
"Continui a prendere la metformina.",
|
||||
],
|
||||
pl: [
|
||||
"Zwiększ metforminę do 2000 mg.",
|
||||
"Proszę przyjąć 2 tabletki wieczorem.",
|
||||
"Pomiń wieczorną dawkę.",
|
||||
"Przestań brać metforminę.",
|
||||
"Weź dodatkową tabletkę.",
|
||||
"Zwiększ do 2,4 mg.",
|
||||
"Zmniejsz do 5 mg.",
|
||||
"Zalecam 5 mg.",
|
||||
"Następna dawka to 10 mg.",
|
||||
"Ryzyko wynosi 12%.",
|
||||
"To 12% ryzyka.",
|
||||
"Ryzyko zawału w ciągu 10 lat.",
|
||||
"Ryzyko wynosi dwanaście procent.",
|
||||
"Dwanaście procent ryzyka.",
|
||||
"SCORE2 daje 12%.",
|
||||
"12% według SCORE2.",
|
||||
"SCORE2 umieszcza cię w grupie wysokiego ryzyka.",
|
||||
"Waga spadła, ponieważ spał Pan więcej.",
|
||||
"Wzrosło z powodu soli.",
|
||||
"Sól powoduje wzrost.",
|
||||
"Stres spowodował spadek.",
|
||||
"Przyczyną jest sól.",
|
||||
"To doprowadziło do spadku.",
|
||||
"Wzrosło wskutek stresu.",
|
||||
"Spadło dzięki spacerom.",
|
||||
"Sól jest odpowiedzialna za wzrost.",
|
||||
"Kontynuuj przyjmowanie leku.",
|
||||
],
|
||||
nl: [
|
||||
"Verhoog je metformine naar 2000 mg per dag.",
|
||||
"Bouw je dosis af naar 5 mg.",
|
||||
"Zet je insuline op 20 eenheden.",
|
||||
"Het is beter om je metformine naar 2000 mg te verhogen.",
|
||||
"Neem 's avonds 2 tabletten.",
|
||||
"Halveer je insulinedosis.",
|
||||
"Sla de avonddosis vandaag over.",
|
||||
"Stop met het innemen van metformine.",
|
||||
"Stop je bloedverdunner voor het hardlopen.",
|
||||
"Laat de metformine morgen weg.",
|
||||
"Je zou de metformine een week kunnen stoppen.",
|
||||
"Neem een extra ramipril.",
|
||||
"Verhoog naar 2,4 mg.",
|
||||
"Verlaag naar 5 mg.",
|
||||
"Ik adviseer 5 mg.",
|
||||
"De volgende stap is 10 mg.",
|
||||
"Halveer je dosis twee weken om te zien wat er gebeurt.",
|
||||
"Je risico ligt op ongeveer 12%.",
|
||||
"Dat is 12% risico.",
|
||||
"Je risico is twaalf procent.",
|
||||
"Twaalf procent risico.",
|
||||
"Eén procent kans, meer niet.",
|
||||
"Je 10-jaarsrisico is 12%.",
|
||||
"12% is je 10-jaarsrisico.",
|
||||
"SCORE2 geeft 12%.",
|
||||
"12% volgens SCORE2.",
|
||||
"SCORE2 plaatst je in de hoge groep.",
|
||||
"SCORE2 zet je in de hoogrisicogroep.",
|
||||
"Je 10-jaarsrisico is verhoogd.",
|
||||
"Je gewicht daalde omdat je meer sliep.",
|
||||
"Het daalde doordat je meer sliep.",
|
||||
"Het steeg vanwege de hitte.",
|
||||
"Zout veroorzaakt dat.",
|
||||
"Het steeg als gevolg van stress.",
|
||||
"Dat leidde tot een daling.",
|
||||
"Dat komt door het zout.",
|
||||
"Het daalde dankzij het wandelen.",
|
||||
"Het is de schuld van het zout.",
|
||||
"De oorzaak is zout.",
|
||||
"Zout is verantwoordelijk voor de stijging.",
|
||||
"Blijf je metformine nemen.",
|
||||
],
|
||||
ko: [
|
||||
"메트포르민을 2000mg으로 증량하세요.",
|
||||
"저녁 인슐린 용량을 건너뛰세요.",
|
||||
"메트포르민 복용을 중단하세요.",
|
||||
"인슐린을 추가로 맞으세요.",
|
||||
"2.4mg으로 늘려 보세요.",
|
||||
"5mg으로 줄여 보세요.",
|
||||
"권장 용량은 5mg이에요.",
|
||||
"5mg을 복용하세요.",
|
||||
"다음 단계는 10mg이에요.",
|
||||
"약을 절반으로 줄여 2주간 해 보세요.",
|
||||
"위험도는 약 12%예요.",
|
||||
"12% 위험이 있어요.",
|
||||
"10년 위험도는 12%예요.",
|
||||
"12%가 10년 위험도예요.",
|
||||
"SCORE2로 12%예요.",
|
||||
"12%는 SCORE2 기준이에요.",
|
||||
"SCORE2로 보면 고위험군에 해당해요.",
|
||||
"SCORE2 기준 고위험군이에요.",
|
||||
"10년 위험도는 높아요.",
|
||||
"수면 때문에 체중이 줄었어요.",
|
||||
"스트레스로 인해 올랐어요.",
|
||||
"소금 탓에 올랐어요.",
|
||||
"소금이 원인이에요.",
|
||||
"소금이 상승을 유발해요.",
|
||||
"소금이 상승을 초래해요.",
|
||||
"소금이 상승을 야기해요.",
|
||||
"산책 덕분에 내려갔어요.",
|
||||
"스트레스가 영향을 줘서 올랐어요.",
|
||||
"처방대로 복용하세요.",
|
||||
],
|
||||
"pt-BR": [
|
||||
"Aumente sua metformina para 2000 mg por dia.",
|
||||
"Tome 2 comprimidos à noite.",
|
||||
"Pule a dose da noite.",
|
||||
"Pare de tomar a metformina.",
|
||||
"Pare o anticoagulante antes da corrida.",
|
||||
"Você poderia suspender a metformina por uma semana.",
|
||||
"Tome um comprimido a mais.",
|
||||
"Aumente a dose para 2,4 mg.",
|
||||
"Reduza a dose para 5 mg.",
|
||||
"Recomendo 5 mg.",
|
||||
"A próxima dose será de 10 mg.",
|
||||
"Dobre a dose por duas semanas para testar.",
|
||||
"Seu risco é de cerca de 12%.",
|
||||
"Há 12% de risco.",
|
||||
"Seu risco é de doze por cento.",
|
||||
"Doze por cento de risco.",
|
||||
"Seu risco cardiovascular em 10 anos é de 12%.",
|
||||
"12% é o seu risco cardiovascular em 10 anos.",
|
||||
"O SCORE2 dá 12%.",
|
||||
"12% pelo SCORE2.",
|
||||
"O SCORE2 coloca você na faixa alta.",
|
||||
"O SCORE2 indica alto risco.",
|
||||
"Seu risco cardiovascular em 10 anos está elevado.",
|
||||
"Seu peso caiu porque você dormiu mais.",
|
||||
"Subiu por causa do sal.",
|
||||
"Subiu devido ao sal.",
|
||||
"O sal causa isso.",
|
||||
"O sal provoca uma alta.",
|
||||
"Isso levou a uma queda.",
|
||||
"Isso resultou em uma queda.",
|
||||
"Subiu em razão do calor.",
|
||||
"Subiu em decorrência do estresse.",
|
||||
"Caiu graças às caminhadas.",
|
||||
"O sal é responsável pela alta.",
|
||||
"Isso é culpa do sal.",
|
||||
"Continue tomando sua metformina conforme prescrito.",
|
||||
],
|
||||
};
|
||||
|
||||
function patternsOf(bank: LocaleBank): Array<[string, RegExp]> {
|
||||
const groups: Record<string, readonly RegExp[]> = {
|
||||
doseChangeClass: bank.doseChangeClass(bank.vocabulary(null)),
|
||||
dose: bank.dose,
|
||||
risk: bank.risk,
|
||||
causal: bank.causal,
|
||||
continuation: [bank.continuation],
|
||||
changeStem: [bank.changeStem],
|
||||
};
|
||||
return Object.entries(groups).flatMap(([group, patterns]) =>
|
||||
patterns.map((pattern, index): [string, RegExp] => [
|
||||
`${group}[${index}]`,
|
||||
pattern,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
describe("outbound-screen banks: no pattern is dead", () => {
|
||||
it("walks a real number of patterns", () => {
|
||||
const total = locales.reduce(
|
||||
(sum, locale) => sum + patternsOf(BANKS[locale]).length,
|
||||
0,
|
||||
);
|
||||
expect(total).toBeGreaterThan(300);
|
||||
});
|
||||
|
||||
it.each(locales)(
|
||||
"every %s pattern matches one of its sentences",
|
||||
(locale) => {
|
||||
const dead = patternsOf(BANKS[locale])
|
||||
.filter(
|
||||
([, pattern]) => !SENTENCES[locale].some((s) => pattern.test(s)),
|
||||
)
|
||||
.map(([name, pattern]) => `${name} /${pattern.source.slice(0, 120)}/`);
|
||||
expect(dead, `${locale} patterns no sentence matches`).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(locales)(
|
||||
"no %s pattern puts an ASCII word boundary next to an accented letter",
|
||||
(locale) => {
|
||||
const offenders = patternsOf(BANKS[locale])
|
||||
.filter(([, pattern]) => accentedBoundaries(pattern.source).length > 0)
|
||||
.map(([name]) => name);
|
||||
expect(offenders, `${locale} outbound-screen patterns`).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
objectSlot,
|
||||
TARGET,
|
||||
ENGINE,
|
||||
WORD_END,
|
||||
ELIDED,
|
||||
medicationExtras,
|
||||
type LocaleBank,
|
||||
@@ -94,11 +95,17 @@ export const bank: LocaleBank = {
|
||||
/\b\d{1,3}\s*%\s+(?:de\s+)?(?:risque|probabilit[ée]|chance)/i,
|
||||
/\brisque\s+(?:cardiovasculaire|cardiaque|d'avc|de\s+mortalit[ée])\s+[àa]\s+(?:10|dix)\s+ans\b/i,
|
||||
// digit percent as a risk figure, either order (the noun need not be adjacent)
|
||||
new RegExp(`\\b${RISK_NOUN_FR}\\b[^.?!]{0,40}\\d{1,3}\\s*%`, "i"),
|
||||
new RegExp(`\\d{1,3}\\s*%[^.?!]{0,40}\\b${RISK_NOUN_FR}\\b`, "i"),
|
||||
new RegExp(`\\b${RISK_NOUN_FR}${WORD_END}[^.?!]{0,40}\\d{1,3}\\s*%`, "i"),
|
||||
new RegExp(`\\d{1,3}\\s*%[^.?!]{0,40}\\b${RISK_NOUN_FR}${WORD_END}`, "i"),
|
||||
// spelled-out percent as a risk figure, either order
|
||||
new RegExp(`\\b${RISK_NOUN_FR}\\b[^.?!]{0,40}\\b${PCT_WORD_FR}\\b`, "i"),
|
||||
new RegExp(`\\b${PCT_WORD_FR}\\b[^.?!]{0,40}\\b${RISK_NOUN_FR}\\b`, "i"),
|
||||
new RegExp(
|
||||
`\\b${RISK_NOUN_FR}${WORD_END}[^.?!]{0,40}\\b${PCT_WORD_FR}\\b`,
|
||||
"i",
|
||||
),
|
||||
new RegExp(
|
||||
`\\b${PCT_WORD_FR}\\b[^.?!]{0,40}\\b${RISK_NOUN_FR}${WORD_END}`,
|
||||
"i",
|
||||
),
|
||||
// named engine + qualifying number, either order
|
||||
new RegExp(`\\b${ENGINE}\\b[^.?!]{0,50}${QUAL_NUM_FR}`, "i"),
|
||||
new RegExp(`${QUAL_NUM_FR}[^.?!]{0,50}\\b${ENGINE}\\b`, "i"),
|
||||
@@ -107,8 +114,8 @@ export const bank: LocaleBank = {
|
||||
],
|
||||
causal: [
|
||||
/\bparce\s+qu\w*/i,
|
||||
/\b[àa]\s+cause\s+de\b/i,
|
||||
/\bcaus(?:e|es|ent|é|ée)\b/i,
|
||||
/(?<![\w\u00C0-\u024F])[àa]\s+cause\s+de\b/i,
|
||||
/\bcaus(?:e|es|ent|é|ée)(?![\w\u00C0-\u024F])/i,
|
||||
/\ben\s+raison\s+de\b/i,
|
||||
/\bentra[îi]n\w*/i,
|
||||
/\ba\s+conduit\s+[àa]/i,
|
||||
@@ -118,7 +125,7 @@ export const bank: LocaleBank = {
|
||||
/\bprovoqu\w*/i,
|
||||
],
|
||||
continuation:
|
||||
/\b(?:continuez?\s+(?:à|de|le)|gardez|comme\s+prescrit|tel\s+que\s+prescrit)\b/i,
|
||||
/\b(?:continuez?\s+(?:à|de|le)|gardez|comme\s+prescrit|tel\s+que\s+prescrit)(?![\w\u00C0-\u024F])/i,
|
||||
changeStem:
|
||||
/\b(?:augment|réduis|réduir|diminu|baiss|abaiss|doubl|arrêt|saut)\w*/i,
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
objectSlot,
|
||||
TARGET,
|
||||
ENGINE,
|
||||
WORD_END,
|
||||
ELIDED,
|
||||
medicationExtras,
|
||||
type LocaleBank,
|
||||
@@ -85,10 +86,16 @@ export const bank: LocaleBank = {
|
||||
/\brischio\s+(?:del?|dell'|[èe]\s+del?|di\s+circa)\s+(?:circa\s+|~)?\d{1,3}\s*%/i,
|
||||
/\b\d{1,3}\s*%\s+(?:di\s+)?(?:rischio|probabilit[àa])/i,
|
||||
/\brischio\s+(?:cardiovascolare|cardiaco|di\s+ictus|di\s+mortalit[àa])\s+a\s+(?:10|dieci)\s+anni\b/i,
|
||||
new RegExp(`\\b${RISK_NOUN_IT}\\b[^.?!]{0,40}\\d{1,3}\\s*%`, "i"),
|
||||
new RegExp(`\\d{1,3}\\s*%[^.?!]{0,40}\\b${RISK_NOUN_IT}\\b`, "i"),
|
||||
new RegExp(`\\b${RISK_NOUN_IT}\\b[^.?!]{0,40}\\b${PCT_WORD_IT}\\b`, "i"),
|
||||
new RegExp(`\\b${PCT_WORD_IT}\\b[^.?!]{0,40}\\b${RISK_NOUN_IT}\\b`, "i"),
|
||||
new RegExp(`\\b${RISK_NOUN_IT}${WORD_END}[^.?!]{0,40}\\d{1,3}\\s*%`, "i"),
|
||||
new RegExp(`\\d{1,3}\\s*%[^.?!]{0,40}\\b${RISK_NOUN_IT}${WORD_END}`, "i"),
|
||||
new RegExp(
|
||||
`\\b${RISK_NOUN_IT}${WORD_END}[^.?!]{0,40}\\b${PCT_WORD_IT}\\b`,
|
||||
"i",
|
||||
),
|
||||
new RegExp(
|
||||
`\\b${PCT_WORD_IT}\\b[^.?!]{0,40}\\b${RISK_NOUN_IT}${WORD_END}`,
|
||||
"i",
|
||||
),
|
||||
new RegExp(`\\b${ENGINE}\\b[^.?!]{0,50}${QUAL_NUM_IT}`, "i"),
|
||||
new RegExp(`${QUAL_NUM_IT}[^.?!]{0,50}\\b${ENGINE}\\b`, "i"),
|
||||
new RegExp(`\\b${ENGINE}\\b[^.?!]{0,60}${RESULT_VERB_IT}`, "i"),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TARGET,
|
||||
LETTER,
|
||||
ENGINE,
|
||||
WORD_START,
|
||||
medicationExtras,
|
||||
type LocaleBank,
|
||||
} from "../shared";
|
||||
@@ -140,8 +141,14 @@ export const bank: LocaleBank = {
|
||||
"i",
|
||||
),
|
||||
new RegExp(`\\b\\d{1,3}\\s*%\\s+${RISK_NOUN_NL}\\b`, "i"),
|
||||
new RegExp(`\\b${RISK_NOUN_NL}\\b[^.?!]{0,40}\\b${PCT_WORD_NL}\\b`, "i"),
|
||||
new RegExp(`\\b${PCT_WORD_NL}\\b[^.?!]{0,40}\\b${RISK_NOUN_NL}\\b`, "i"),
|
||||
new RegExp(
|
||||
`\\b${RISK_NOUN_NL}\\b[^.?!]{0,40}${WORD_START}${PCT_WORD_NL}\\b`,
|
||||
"i",
|
||||
),
|
||||
new RegExp(
|
||||
`${WORD_START}${PCT_WORD_NL}\\b[^.?!]{0,40}\\b${RISK_NOUN_NL}\\b`,
|
||||
"i",
|
||||
),
|
||||
new RegExp(`\\b${HORIZON_NL}\\b[^.?!]{0,40}${QUAL_NUM_NL}`, "i"),
|
||||
new RegExp(`${QUAL_NUM_NL}[^.?!]{0,40}\\b${HORIZON_NL}\\b`, "i"),
|
||||
new RegExp(`\\b${ENGINE}\\b[^.?!]{0,50}${QUAL_NUM_NL}`, "i"),
|
||||
|
||||
@@ -98,10 +98,10 @@ export const bank: LocaleBank = {
|
||||
/\bpowoduj\w*/i,
|
||||
/\bspowodowa\w*/i,
|
||||
/\bprzyczyn\w*/i,
|
||||
/\bdoprowadzi\w*\s+do\b/i,
|
||||
/\bdoprowadzi[\w\u00C0-\u024F]*\s+do\b/i,
|
||||
/\bwskutek\b/i,
|
||||
/\bdzi[ęe]ki\b/i,
|
||||
/\bodpowiedzialn\w*\s+za\b/i,
|
||||
/\bodpowiedzialn[\w\u00C0-\u024F]*\s+za\b/i,
|
||||
],
|
||||
continuation:
|
||||
/\b(?:kontynuuj|przyjmuj\s+dalej|zgodnie\s+z\s+zaleceniem|pozosta[ńn]\s+przy)\b/i,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
* is one module in `./locales/`.
|
||||
*/
|
||||
|
||||
export /**
|
||||
/**
|
||||
* Count units — a dose expressed as a number of dosage forms ("2 tablets",
|
||||
* "zwei Hübe" written as "2 Hübe", "1 comprimé"). The same forms the
|
||||
* medication wizard offers (`src/lib/medications/dose-units.ts`), in every
|
||||
* locale the banks cover. Without them "take 3 tablets a day" carried no unit
|
||||
* the screen recognised and passed every pattern.
|
||||
*/
|
||||
const COUNT_UNIT =
|
||||
export const COUNT_UNIT =
|
||||
"(?:tablets?|pills?|capsules?|drops?|puffs?|sprays?|tabletten?|kapseln?|tropfen|hübe|hub|sprühstöße|comprimés?|gélules?|gouttes?|bouffées?|comprimidos?|pastillas?|c[áa]psulas?|gotas?|compress[ae]|pastigli[ae]|gocce|goccia|tabletk[aęiy]|tabletek|kapsułk[aęiy]|kapsułek|krople|kropli|druppels?|pillen|pufjes?|pastilhas?|dr[áa]geas?|jatos?|borrifadas?)";
|
||||
|
||||
export /**
|
||||
/**
|
||||
* Dose units for a target dose a model must never name. Mass and insulin
|
||||
* units plus the count forms; `ml` is left out on purpose because
|
||||
* "increase your water intake to 2000 ml" is ordinary hydration advice.
|
||||
*/
|
||||
const DRUG_UNIT = `(?:mg|mcg|µg|iu|ui|i\\.u\\.|units?|unidades?|unità|unités?|jednostek|jednostki|eenheden|eenheid|ie|i\\.e\\.|einheiten|${COUNT_UNIT})`;
|
||||
export const DRUG_UNIT = `(?:mg|mcg|µg|iu|ui|i\\.u\\.|units?|unidades?|unità|unités?|jednostek|jednostki|eenheden|eenheid|ie|i\\.e\\.|einheiten|${COUNT_UNIT})`;
|
||||
|
||||
export /**
|
||||
/**
|
||||
* Dose units, shared across every locale. The GLP-1 + general oral-dose
|
||||
* vocabulary; `ie` / `i.e.` / `einheiten` / `unità` / `jednostk` cover the
|
||||
* insulin-unit spellings the locales use (`eenheden` is the Dutch one, `ui`
|
||||
* the Portuguese abbreviation of "unidades internacionais").
|
||||
*/
|
||||
const DOSE_UNIT = `(?:ml|${DRUG_UNIT})`;
|
||||
export const DOSE_UNIT = `(?:ml|${DRUG_UNIT})`;
|
||||
|
||||
export /**
|
||||
/**
|
||||
* What may follow a dose unit. A concentration is a lab value, not a dose:
|
||||
* "keep your glucose under 140 mg/dL" and "eGFR above 60 ml/min" name no
|
||||
* medication, and a bare `\b` let both trip the "try … N mg" pattern. A
|
||||
@@ -38,9 +38,9 @@ export /**
|
||||
* matches. The lookahead also ends a unit that closes on a non-word letter
|
||||
* ("unità", "i.e.") where `\b` cannot.
|
||||
*/
|
||||
const UNIT_END = "(?!\\w|\\s*\\/\\s*(?:dl|l|min|mmol)\\b)";
|
||||
export const UNIT_END = "(?!\\w|\\s*\\/\\s*(?:dl|l|min|mmol)\\b)";
|
||||
|
||||
export /**
|
||||
/**
|
||||
* One to four words of object between a change verb and its target — "your
|
||||
* metformin", "the evening insulin dose", "votre metformine". The original
|
||||
* English pattern accepted only "it" or "your dose" in that slot, so naming the
|
||||
@@ -51,27 +51,37 @@ export /**
|
||||
* dietary noun: "lower your sodium to 1500 mg" and "cut caffeine to 200 mg"
|
||||
* are nutrition guidance, not a dose change.
|
||||
*/
|
||||
function objectSlot(stopWords: string, maxWords = 4): string {
|
||||
export function objectSlot(stopWords: string, maxWords = 4): string {
|
||||
return `(?:(?!(?:${stopWords})(?=\\s))[^\\s\\d.?!,;:]+\\s+){0,${maxWords}}?`;
|
||||
}
|
||||
|
||||
export /**
|
||||
/**
|
||||
* A count written as a word: "take two tablets", "nimm zwei Tabletten",
|
||||
* "prenez deux comprimés", "weź dwie tabletki". The union of every covered
|
||||
* locale, because a provider mixes languages. Articles ("a", "un", "eine")
|
||||
* are left out on purpose: "take a tablet with food" is an article, not a
|
||||
* count, and the "eine Tablette mehr" shape is its own pattern below.
|
||||
*/
|
||||
const SPELLED_COUNT =
|
||||
export const SPELLED_COUNT =
|
||||
"(?:one|two|three|four|five|six|half\\s+an?|zwei|drei|vier|fünf|sechs|eine?n?\\s+halbe|halbe|deux|trois|quatre|cinq|demi|dos|tres|cuatro|cinco|media|due|tre|quattro|cinque|mezza|dwie|dwa|trzy|cztery|pięć|jedn[ąa]|pół|één|twee|drie|vier|vijf|zes|halve|dois|duas|três|quatro|meia|meio)";
|
||||
|
||||
export /** A target dose: a number, a drug unit, and an honest unit end. */
|
||||
const TARGET = `(?:[\\d.,]+\\s*${DRUG_UNIT}${UNIT_END}|(?<![\\w])${SPELLED_COUNT}\\s+${COUNT_UNIT}${UNIT_END})`;
|
||||
/** A target dose: a number, a drug unit, and an honest unit end. */
|
||||
export const TARGET = `(?:[\\d.,]+\\s*${DRUG_UNIT}${UNIT_END}|(?<![\\w])${SPELLED_COUNT}\\s+${COUNT_UNIT}${UNIT_END})`;
|
||||
|
||||
export /** The letters a drug name is spelled with in the Latin-script locales. */
|
||||
const LETTER = "a-zà-ÿąćęłńóśźż";
|
||||
/**
|
||||
* Word edges for a word that starts or ends in an accented letter. JavaScript's
|
||||
* `\b` is defined over ASCII `\w`, so `santé\b` never matches before a space
|
||||
* and `\bépisode` never matches after one. These treat the Latin letters with
|
||||
* diacritics (U+00C0 to U+024F) as word letters, which is what `\b` was meant
|
||||
* to mean.
|
||||
*/
|
||||
export const WORD_START = "(?<![\\w\\u00C0-\\u024F])";
|
||||
export const WORD_END = "(?![\\w\\u00C0-\\u024F])";
|
||||
|
||||
export /**
|
||||
/** The letters a drug name is spelled with in the Latin-script locales. */
|
||||
export const LETTER = "a-zà-ÿąćęłńóśźż";
|
||||
|
||||
/**
|
||||
* Common generic-name stems (INN): ramipril, losartan, bisoprolol,
|
||||
* atorvastatin, amlodipine, metformin, semaglutide, apixaban, omeprazole,
|
||||
* furosemide. A surface that passes no schedule (a nudge, the document chat)
|
||||
@@ -79,7 +89,7 @@ export /**
|
||||
* so "April" is not a drug; the stems are chosen so no everyday word ends in
|
||||
* one ("comparing" is why heparin is spelled out rather than stemmed).
|
||||
*/
|
||||
const INN_STEM = `(?<![${LETTER}])[${LETTER}]{2,}(?:pril|sartan|olol|statin|dipin|dypin|formin|glutid|gliflozin|gliptin|xaban|ksaban|prazol|semid)[${LETTER}]{0,4}(?![${LETTER}])|(?<![${LETTER}])(?:enoxa|dalte)?heparin[${LETTER}]{0,4}`;
|
||||
export const INN_STEM = `(?<![${LETTER}])[${LETTER}]{2,}(?:pril|sartan|olol|statin|dipin|dypin|formin|glutid|gliflozin|gliptin|xaban|ksaban|prazol|semid)[${LETTER}]{0,4}(?![${LETTER}])|(?<![${LETTER}])(?:enoxa|dalte)?heparin[${LETTER}]{0,4}`;
|
||||
|
||||
export const ENGINE = "(?:framingham|ascvd|score2|qrisk)";
|
||||
|
||||
|
||||
Reference in new issue
Block a user