Merge branch 'release/v1.45.0' into fix/v145-review-data

This commit is contained in:
Marc-André Bombeck committed 2026-10-22 21:48:34 +02:00
commit c10446f0d7
15 files changed
+523 -111

No files matched your search

@@ -0,0 +1,195 @@
/**
* What the dose screen must let through: realistic, harmless Coach replies
* in every language. Advice to talk to the doctor, negated warnings,
* questions, conditions, a "dose" of caffeine, salt or exercise, and a
* correct restatement of the person's own plan. The positive corpus sits in
* `outbound-screen.test.ts` (`DOSE_CLASS_VIOLATIONS`); a pattern that widens
* the class has to keep both green.
*/
import { describe, expect, it } from "vitest";
import type { Locale } from "@/lib/i18n/config";
import {
CONVERSATIONAL_CONTRACTS,
screenModelOutput,
} from "../outbound-screen";
const PLAN = [
{ name: "Ramipril", dose: 5 },
{ name: "Metformin", dose: 1000 },
];
const HARMLESS: Record<Locale, readonly string[]> = {
en: [
"Talk to your doctor before you change your dose.",
"Discuss with your doctor: reduce your dose?",
"If your doctor agrees, then reduce your dose gradually.",
"Don't reduce your dose for now.",
"Never reduce your dose for a missed meal without guidance.",
"Do not take half a tablet unless your pharmacist says it is scored.",
"Increase your dose? Not without your doctor.",
"Should you lower your insulin? That is a question for your diabetes team.",
"Reduce your dose of caffeine in the afternoon.",
"Cut your salt a little this week.",
"Increase your activity gradually.",
"Your doctor said to reduce your dose for a week.",
"You take metformin 1000 mg twice daily, as your plan says.",
"Keep taking ramipril 5 mg as before.",
"Reduce your sugar intake by half.",
"Ask your pharmacist whether the tablet can be split in half.",
],
de: [
"Sprich mit deiner Ärztin, bevor du die Dosis änderst.",
"Erhöhe dein Insulin heute nicht, auch wenn der Wert hoch ist.",
"Reduzieren Sie die Dosis jetzt nicht eigenmächtig.",
"Senke die Dosis bis zum Arzttermin nicht.",
"Nimm die halbe Dosis nicht ohne Rücksprache.",
"Teile die Tablette in zwei Hälften nur, wenn deine Apotheke zustimmt.",
"Wenn deine Ärztin zustimmt, reduziere die Dosis schrittweise.",
"Soll ich die Dosis erhöhen? Das klärst du mit deiner Praxis.",
"Reduziere den Zucker um die Hälfte.",
"Reduziere deinen Salzkonsum.",
"Steigere deine Aktivität langsam.",
"Frag deine Ärztin, ob du die halbe Dosis nehmen kannst.",
"Du nimmst Metformin 1000 mg zweimal täglich, wie geplant.",
"Nimm Ramipril 5 mg wie bisher weiter.",
"Erhöhe die Dosis also für ein paar Tage nicht.",
],
fr: [
"Parlez à votre médecin avant de changer votre dose.",
"Ne réduisez pas votre dose maintenant.",
"Ne prenez pas une demi-dose sans avis.",
"Faut-il diminuer votre dose ? Demandez à votre médecin.",
"Si votre médecin le juge utile, il peut diminuer votre dose.",
"Réduisez le sel de moitié.",
"Augmentez votre activité progressivement.",
"Diminuez votre café l’après-midi.",
"Vous prenez metformine 1000 mg deux fois par jour.",
"Demandez à votre pharmacien si le comprimé peut être coupé en deux.",
"Il est préférable que vous ne diminuiez pas votre dose maintenant.",
"Avant de diminuer votre dose, parlez à votre médecin.",
"Prenez une demi-heure pour marcher.",
"Augmentez votre consommation d’eau.",
"N’augmentez pas votre insuline sans votre médecin.",
],
es: [
"Hable con su médico antes de que reduzca la dosis.",
"Consulte si conviene que reduzca la dosis.",
"Espere a su cita antes de que aumente la insulina.",
"Cuando su médico lo indique, reduzca la dosis.",
"No reduzca la dosis hoy.",
"¿Debería reducir la dosis? Pregunte a su médico.",
"Es importante que no reduzca la dosis hoy.",
"Reduzca el azúcar a la mitad.",
"Aumente su actividad poco a poco.",
"Tome media hora para caminar.",
"Usted toma metformina 1000 mg dos veces al día.",
"Pregunte a su farmacéutico si puede partir la pastilla.",
"Le recomiendo que no aumente la dosis ahora.",
"Antes de que baje la dosis, consulte a su médico.",
"Reduzca el café por la tarde.",
],
it: [
"Parli con il medico prima di cambiare la dose.",
"Non riduca la dose ora.",
"Se il medico decide, riduca la dose per gradi.",
"Chieda al medico se può prendere mezza dose.",
"Ridurre la dose? Solo se il medico lo consiglia.",
"Prima che riduca la dose, ne parli con il medico.",
"È importante che lei non riduca la dose ora.",
"Riduca lo zucchero della metà.",
"Aumenti l'attività gradualmente.",
"Prenda mezz'ora per camminare.",
"Lei prende metformina 1000 mg due volte al giorno.",
"Riduca il caffè nel pomeriggio.",
"Chieda al farmacista se la compressa si può dividere.",
"Non aumenti l'insulina senza il medico.",
"Aumenti l'acqua durante il giorno.",
],
pl: [
"Porozmawiaj z lekarzem, zanim zmienisz dawkę.",
"Nie zwiększaj dawki teraz.",
"Jeśli lekarz zaleci, zmniejsz dawkę stopniowo.",
"Lekarz może zdecydować: zmniejsz dawkę leku.",
"Zapytaj lekarza, czy możesz przyjmować pół dawki.",
"Zmniejszyć dawkę? Tylko jeśli lekarz tak zdecyduje.",
"Zmniejsz dawkę kofeiny.",
"Zmniejsz dawkę cukru.",
"Zwiększ dawkę ruchu.",
"Zmniejsz spożycie soli o połowę.",
"Zwiększ aktywność stopniowo.",
"Przyjmujesz metforminę 1000 mg dwa razy dziennie.",
"Odstaw telefon przed snem.",
"Nie zmniejszaj dawki insuliny bez lekarza.",
"Zapytaj farmaceutę, czy tabletkę można podzielić na pół.",
],
nl: [
"Overleg met je arts voordat je je dosis aanpast.",
"Verlaag je dosis nu niet.",
"Verhoog je insuline vandaag niet zelf.",
"Verlaag je dosis vanaf morgen niet zonder overleg.",
"Vraag je arts: verlaag je dosis?",
"Vraag je arts of je een halve dosis mag nemen.",
"Verlaag je dosis alleen als je arts dat zegt.",
"Verlaag je suiker met de helft.",
"Verhoog je activiteit geleidelijk.",
"Neem een half uur de tijd.",
"Je neemt metformine 1000 mg twee keer per dag.",
"Vraag je apotheker of de tablet doormidden mag.",
"Verminder je koffie in de middag.",
"Verdubbel je stappen twee weken lang.",
"Neem geen halve dosis zonder overleg.",
],
"pt-BR": [
"Converse com seu médico antes de mudar a dose.",
"Não reduza a dose agora.",
"É importante que você não reduza a dose agora.",
"Antes que você reduza a dose, fale com seu médico.",
"Pergunte ao seu médico se você pode tomar meia dose.",
"Reduzir a dose? Só se o médico indicar.",
"Reduza o açúcar pela metade.",
"Aumente sua atividade aos poucos.",
"Tome meia hora para caminhar.",
"Você toma metformina 1000 mg duas vezes ao dia.",
"Corte o pão ao meio.",
"Reduza o café à tarde.",
"Pergunte ao farmacêutico se o comprimido pode ser partido.",
"Não aumente a insulina sem o médico.",
"Aumente a ingestão de água.",
],
ko: [
"의사와 상의 후 복용량을 늘리세요.",
"복용량을 늘리세요라고 말씀드릴 수는 없어요.",
"의사와 상의하지 않고 복용량을 줄이지 마세요.",
"약 복용량을 바꾸고 싶으시면 먼저 의사와 상의하세요.",
"용량을 줄이지 마세요.",
"복용량을 늘려야 할까요? 주치의와 이야기해 보세요.",
"걸음 수를 늘리세요.",
"물 섭취를 늘리세요.",
"약 2주 동안 걷는 시간을 늘리세요.",
"커피를 줄이세요.",
"하루 두 번 1000mg을 복용하고 계세요.",
"약사에게 알약을 반으로 나눠도 되는지 물어보세요.",
"설탕 섭취를 절반으로 줄이세요.",
"운동량을 천천히 늘리세요.",
"담배를 끊으세요.",
],
};
describe("dose screen false-positive corpus", () => {
for (const [locale, sentences] of Object.entries(HARMLESS) as Array<
[Locale, readonly string[]]
>) {
it(`${locale} carries at least 15 harmless replies`, () => {
expect(sentences.length).toBeGreaterThanOrEqual(15);
});
it.each(sentences)(`passes in ${locale}: %s`, (text) => {
const d = screenModelOutput(text, locale, CONVERSATIONAL_CONTRACTS, {
dosePlan: PLAN,
medicationNames: PLAN.map((entry) => entry.name),
});
expect(d.block).toBe(false);
});
}
});
@@ -18,7 +18,7 @@ describe("screenModelOutput — dose exemption gated on the plan", () => {
it("passes a continuation dose that MATCHES the user's plan", () => {
const d = screenModelOutput(
"You should keep taking your prescribed 7.5 mg exactly as directed.",
"You should keep taking your prescribed semaglutide 7.5 mg exactly as directed.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
@@ -30,7 +30,7 @@ describe("screenModelOutput — dose exemption gated on the plan", () => {
// The plan says 7.5; "keep taking your 15 mg" is a wrong maintenance
// dose and must not ride the continuation exemption.
const d = screenModelOutput(
"You should keep taking your 15 mg as prescribed.",
"You should keep taking your semaglutide 15 mg as prescribed.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
@@ -61,12 +61,12 @@ describe("screenModelOutput — dose exemption gated on the plan", () => {
}
});
it("passes the same sentences when the plan holds that dose", () => {
it("passes a named continuation when the plan holds that dose for it", () => {
for (const [text, locale] of [
["Take 40 mg as before.", "en"],
["Tome 40 mg como antes.", "pt-BR"],
["Nimm 40 mg wie bisher.", "de"],
["Neem 40 mg zoals eerder.", "nl"],
["Take atorvastatin 40 mg as before.", "en"],
["Tome atorvastatina 40 mg como antes.", "pt-BR"],
["Nimm Atorvastatin 40 mg wie bisher.", "de"],
["Neem atorvastatine 40 mg zoals eerder.", "nl"],
] as const) {
expect(
screenModelOutput(text, locale, CONVERSATIONAL_CONTRACTS, {
@@ -77,6 +77,43 @@ describe("screenModelOutput — dose exemption gated on the plan", () => {
}
});
it("blocks an amount with no medication named beside it, even on the plan", () => {
for (const text of [
"Take 40 mg as before.",
"You should keep taking your prescribed 7.5 mg exactly as directed.",
]) {
expect(
screenModelOutput(text, "en", CONVERSATIONAL_CONTRACTS, {
dosePlan: PLAN,
}).block,
text,
).toBe(true);
}
});
it("blocks a brand the plan does not carry, and swapped doses", () => {
for (const text of [
"Take Eliquis 5 mg as before.",
"Take Xarelto 40 mg as before.",
"Take ramipril 40 mg and atorvastatin 5 mg as before.",
]) {
expect(
screenModelOutput(text, "en", CONVERSATIONAL_CONTRACTS, {
dosePlan: PLAN,
}).block,
text,
).toBe(true);
}
expect(
screenModelOutput(
"Take ramipril 5 mg and atorvastatin 40 mg as before.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
).block,
).toBe(false);
});
it("holds a named medication to its own dose, not to any dose on the plan", () => {
// 40 mg is on the plan, but for atorvastatin, not ramipril.
expect(
@@ -137,7 +174,7 @@ describe("screenModelOutput — dose exemption gated on the plan", () => {
it("matches a comma-decimal planned dose (DE reader)", () => {
const d = screenModelOutput(
"Nimm weiterhin deine 7,5 mg wie verordnet.",
"Nimm weiterhin dein Semaglutid 7,5 mg wie verordnet.",
"de",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
@@ -290,7 +290,7 @@ describe("screenModelOutput — dose continuation-exclusion (D7)", () => {
it("PASSES an adherence-supporting continuation of the planned dose", () => {
const d = screenModelOutput(
"You should keep taking your prescribed 7.5 mg exactly as directed.",
"You should keep taking your prescribed semaglutide 7.5 mg exactly as directed.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
@@ -343,7 +343,7 @@ describe("screenModelOutput — dose continuation-exclusion (D7)", () => {
// "du solltest … 7,5 mg" trips the DE consider/should dose pattern; the
// "weiterhin … wie verordnet" maintenance anchor exempts it.
const d = screenModelOutput(
"Du solltest weiterhin deine 7,5 mg wie verordnet einnehmen.",
"Du solltest weiterhin dein Semaglutid 7,5 mg wie verordnet einnehmen.",
"de",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: PLAN },
@@ -796,7 +796,7 @@ describe("screenModelOutput — the dose-change class, every locale", () => {
// On schedule: the maintenance anchor exempts it.
expect(
screenModelOutput(
"Take your 1000 mg as usual.",
"Take your metformin 1000 mg as usual.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: [{ name: "Metformin", dose: 1000 }] },
@@ -805,7 +805,7 @@ describe("screenModelOutput — the dose-change class, every locale", () => {
// Off schedule: the same phrasing names a dose the user is not on.
expect(
screenModelOutput(
"Take your 2000 mg as usual.",
"Take your metformin 2000 mg as usual.",
"en",
CONVERSATIONAL_CONTRACTS,
{ dosePlan: [{ name: "Metformin", dose: 1000 }] },
+48 -18
View File
@@ -315,8 +315,10 @@ export const INSIGHTS_CONTRACTS: readonly OutboundContract[] = [
* Continuation-exclusion for the dose bank (v1.32.7 — D7). A sentence that
* matches a dose pattern is EXEMPT only when it is anchored to a maintenance
* object ("keep taking … as prescribed"), carries no change stem, AND every
* amount it states is one the person is on for the medication it names. So
* "keep taking your prescribed 7.5 mg" passes for someone on 7.5 mg, while
* amount it states sits beside one of the person's medications, named, at
* exactly that planned dose. So "keep taking your semaglutide 7.5 mg as
* prescribed" passes for someone on semaglutide 7.5 mg, an unnamed "keep
* taking your 7.5 mg" does not, while
* "you should keep in mind that trying 5 mg" (no maintenance anchor) and
* "you should continue tapering to 2.4 mg" (a change stem present) stay
* blocked. The direction is exclusion-of-continuation, never
@@ -364,26 +366,53 @@ function sameDose(value: number, planned: number): boolean {
const INN_STEM_RE = new RegExp(INN_STEM, "i");
/**
* True when every amount a continuation sentence states is on the person's
* plan for the medication it names. A sentence naming one of their
* medications is held to that medication's doses; one naming a drug outside
* the plan is never trusted; one naming none is held to the whole plan.
* Where one stated amount ends and the next begins: a comma before a space
* (a decimal comma, "7,5", is not one), a semicolon, "and" / "or".
*/
const CLAUSE_SPLIT =
/,(?=\s)|;|\s(?:and|und|en|et|y|e|i|oraz|plus|sowie|ou|o|or|oder)\s|그리고|및/i;
/**
* True when every amount a continuation sentence states is paired with one
* of the person's medications, named beside it, at exactly that dose. Each
* clause that states an amount must name exactly one medication on the plan
* (as it is written on the plan, the only alias the screen can know), and
* every amount in it must be one of that medication's planned doses. An
* amount with no name beside it, a name the plan does not carry (a brand
* like "Eliquis" is simply not a plan name; a generic stem like "apixaban"
* is recognised and refused), or a dose that belongs to another medication
* on the plan ("ramipril 500 mg and metformin 5 mg") is never trusted.
*/
function continuationMatchesPlan(
sentence: string,
plan: readonly ScheduledDose[] | undefined,
): boolean {
const values = sentenceDoseValues(sentence);
if (values.length === 0 || !plan || plan.length === 0) return false;
const named = plan.filter((entry) => {
if (!plan || plan.length === 0) return false;
const matchers = plan.map((entry) => {
const alternation = medicationNameAlternation([entry.name]);
return alternation !== null && new RegExp(alternation, "i").test(sentence);
return alternation === null ? null : new RegExp(alternation, "gi");
});
if (named.length === 0 && INN_STEM_RE.test(sentence)) return false;
const pool = named.length > 0 ? named : plan;
return values.every((value) =>
pool.some((entry) => sameDose(value, entry.dose)),
);
let stated = 0;
for (const clause of sentence.split(CLAUSE_SPLIT)) {
const values = sentenceDoseValues(clause);
if (values.length === 0) continue;
stated += values.length;
const named = plan.filter((_, i) => matchers[i]?.test(clause) ?? false);
matchers.forEach((m) => m && (m.lastIndex = 0));
const names = new Set(named.map((entry) => entry.name.toLowerCase()));
if (names.size !== 1) return false;
// A generic name left once the plan's own names are taken out is a drug
// the plan does not carry.
const rest = matchers.reduce(
(text, m) => (m ? text.replace(m, " ") : text),
clause,
);
if (INN_STEM_RE.test(rest)) return false;
if (!values.every((v) => named.some((e) => sameDose(v, e.dose)))) {
return false;
}
}
return stated > 0;
}
/** True when a dose-change imperative trips, honouring the continuation rule. */
@@ -427,9 +456,10 @@ function doseTrips(
export interface ScreenOptions {
/**
* The person's active medications with their dose magnitudes. A "keep
* taking your N mg" is trusted only when N is on this plan for the
* medication the sentence names; without the plan, a stated amount is never
* exempt.
* taking your X N mg" is trusted only when X is a medication on this plan,
* named beside the amount, planned at exactly N; an unnamed amount, a name
* the plan does not carry, or a dose of another medication is never
* exempt, and without the plan no stated amount is.
*/
dosePlan?: readonly ScheduledDose[];
/**
+30 -12
View File
@@ -7,6 +7,9 @@ import {
LETTER,
ENGINE,
medicationExtras,
unlessDeferred,
WORD_START,
WORD_END,
type LocaleBank,
} from "../shared";
@@ -59,6 +62,9 @@ const RISK_VERDICT_DE = `(?:ist|liegt|erscheint|wirkt|bleibt|zeigt|deutet\\s+auf
const DE_MED_CLASS =
"\\S*(?:blutverdünner\\w*|gerinnungshemmer\\w*|antikoagul\\w*|statin\\w*|betablocker\\w*|diuretik\\w*|wassertablette\\w*|blutdrucksenker\\w*|antidepressiv\\w*|schlaftablette\\w*)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const DE_DEFERRAL = `\\?|${WORD_START}(?:nicht|kein\\w*|nie|niemals|nur\\s*,?\\s*(?:wenn|falls|nach)|erst\\s*,?\\s*(?:wenn|nach)|ohne|frag\\w*|besprich\\w*|sprich\\s+mit|rücksprache|ärzt\\w*|arzt\\w*|apothek\\w*|hausärzt\\w*|diabetolog\\w*|fachperson\\w*|praxis)${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${DE_MED_NOUN}|${medicationExtras(DE_MED_CLASS, names)})`,
@@ -109,24 +115,36 @@ export const bank: LocaleBank = {
// Hälfte", "nehmen Sie nur die halbe Dosis", "teilen Sie die Tablette in
// zwei Hälften". "Teilen" alone also means "share", so the split needs
// its "in Hälften".
new RegExp(
`\\b(?:reduzier(?:e|en)?|verringer(?:e|n)?|senk(?:e|en)?|kürz(?:e|en)?)(?:\\s+sie)?\\s+${DE_SLOT}${med}\\s+um\\s+(?:die\\s+)?hälfte`,
"i",
unlessDeferred(
DE_DEFERRAL,
new RegExp(
`\\b(?:reduzier(?:e|en)?|verringer(?:e|n)?|senk(?:e|en)?|kürz(?:e|en)?)(?:\\s+sie)?\\s+${DE_SLOT}${med}\\s+um\\s+(?:die\\s+)?hälfte`,
"i",
),
),
new RegExp(
`\\b(?:nimm|nehmen\\s+sie|spritz|spritzen\\s+sie|injizier(?:e)?|injizieren\\s+sie)\\s+(?:nur\\s+)?(?:die\\s+|eine\\s+)?(?:halbe|hälfte\\s+(?:der|deiner|ihrer))\\s+${DE_SHORT}${med}`,
"i",
unlessDeferred(
DE_DEFERRAL,
new RegExp(
`\\b(?:nimm|nehmen\\s+sie|spritz|spritzen\\s+sie|injizier(?:e)?|injizieren\\s+sie)\\s+(?:nur\\s+)?(?:die\\s+|eine\\s+)?(?:halbe|hälfte\\s+(?:der|deiner|ihrer))\\s+${DE_SHORT}${med}`,
"i",
),
),
new RegExp(
`\\bteil(?:e|en)?(?:\\s+sie)?\\s+${DE_SLOT}${med}\\s+(?:in\\s+(?:zwei\\s+)?hälften|in\\s+der\\s+mitte|durch\\s+zwei)`,
"i",
unlessDeferred(
DE_DEFERRAL,
new RegExp(
`\\bteil(?:e|en)?(?:\\s+sie)?\\s+${DE_SLOT}${med}\\s+(?:in\\s+(?:zwei\\s+)?hälften|in\\s+der\\s+mitte|durch\\s+zwei)`,
"i",
),
),
// A bare change imperative on a medication, no target: "reduzieren Sie
// die Dosis.", "erhöhe dein Insulin etwas". The noun closes the clause or
// meets a time or degree word; "reduziere die Dosis nicht" does neither.
new RegExp(
`\\b(?:erhöh(?:e|en)?|steiger(?:e|n)?|reduzier(?:e|en)?|senk(?:e|en)?|verringer(?:e|n)?)(?:\\s+sie)?\\s+${DE_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:jetzt|heute|morgen|etwas|leicht|langsam|schrittweise|ab\\s+sofort|für|bis)\\b)`,
"i",
unlessDeferred(
DE_DEFERRAL,
new RegExp(
`\\b(?:erhöh(?:e|en)?|steiger(?:e|n)?|reduzier(?:e|en)?|senk(?:e|en)?|verringer(?:e|n)?)(?:\\s+sie)?\\s+${DE_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:jetzt|heute|morgen|etwas|leicht|langsam|schrittweise|ab\\s+sofort|für|bis)\\b)`,
"i",
),
),
],
dose: [
@@ -6,6 +6,7 @@ import {
TARGET,
ENGINE,
medicationExtras,
unlessDeferred,
type LocaleBank,
} from "../shared";
@@ -67,6 +68,9 @@ const RISK_VERDICT_EN = `(?:is|are|looks?|appears?|seems?|sits?|remains?|comes?\
const EN_MED_CLASS =
"(?:blood[- ]?thinners?|anti-?coagulants?|anti-?platelets?|statins?|beta[- ]?blockers?|diuretics?|water\\s+pills?|ace\\s+inhibitors?|steroids?|antidepressants?|sleeping\\s+pills?)\\b";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const EN_DEFERRAL = `\\?|\\b(?:not|never|don'?t|doesn'?t|no|unless|only\\s+(?:if|when|after)|without|ask|discuss|talk\\s+to|check\\s+with|doctor|physician|gp|pharmacist|prescriber|clinician|nurse|specialist|care\\s+team|diabetes\\s+team)\\b`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${EN_MED_NOUN}|${medicationExtras(EN_MED_CLASS, names)})`,
@@ -117,21 +121,30 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "reduce your dose by half",
// "take half a dose". A half hour or half your sugar names no medication.
new RegExp(
`${EN_LEAD}(?:reduce|cut|lower|decrease|drop|split|break|divide)\\s+${EN_SHORT}${med}\\s+(?:by|in)\\s+(?:a\\s+)?half\\b`,
"im",
unlessDeferred(
EN_DEFERRAL,
new RegExp(
`${EN_LEAD}(?:reduce|cut|lower|decrease|drop|split|break|divide)\\s+${EN_SHORT}${med}\\s+(?:by|in)\\s+(?:a\\s+)?half\\b`,
"im",
),
),
new RegExp(
`${EN_LEAD}(?:take|use|inject|give\\s+yourself)\\s+(?:only\\s+|just\\s+)?half\\s+(?:of\\s+)?(?:(?:a|an|the|your)\\s+)?${EN_SHORT}${med}`,
"im",
unlessDeferred(
EN_DEFERRAL,
new RegExp(
`${EN_LEAD}(?:take|use|inject|give\\s+yourself)\\s+(?:only\\s+|just\\s+)?half\\s+(?:of\\s+)?(?:(?:a|an|the|your)\\s+)?${EN_SHORT}${med}`,
"im",
),
),
// A bare change imperative on a medication, no target: "reduce your
// dose.", "increase your insulin slightly". The medication noun has to
// close the clause or meet a time or degree word, so "reduce your dose of
// stress" and "cut your medication costs" are not read as one.
new RegExp(
`${EN_LEAD}(?:increase|raise|up|boost|bump|reduce|lower|decrease|cut|drop)\\s+${EN_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:now|today|tonight|tomorrow|a\\s+(?:bit|little)|slightly|gradually|somewhat|this\\s+week|for|until|from\\s+now)\\b)`,
"im",
unlessDeferred(
EN_DEFERRAL,
new RegExp(
`${EN_LEAD}(?:increase|raise|up|boost|bump|reduce|lower|decrease|cut|drop)\\s+${EN_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:now|today|tonight|tomorrow|a\\s+(?:bit|little)|slightly|gradually|somewhat|this\\s+week|for|until|from\\s+now)\\b)`,
"im",
),
),
],
dose: [
@@ -7,6 +7,9 @@ import {
LETTER,
ENGINE,
medicationExtras,
unlessDeferred,
WORD_START,
WORD_END,
type LocaleBank,
} from "../shared";
@@ -35,6 +38,9 @@ const RESULT_VERB_ES =
const ES_MED_CLASS =
"(?:anticoagulantes?|antiagregantes?|estatinas?|betabloqueantes?|diuréticos?|antidepresivos?)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const ES_DEFERRAL = `\\?|¿|${WORD_START}(?:no|nunca|sin|solo\\s+si|sólo\\s+si|(?:antes\\s+de|hasta|después\\s+de|conviene|si)\\s+que|consult\\w*|pregunt\\w*|hable\\s+con|médic[oa]s?|doctor[a]?|farmac[ée]utic[oa]|enfermer[oa])${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${ES_MED_NOUN}|${medicationExtras(ES_MED_CLASS, names)})`,
@@ -67,19 +73,28 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "reduzca la dosis a la mitad",
// "tome media dosis", "tome la mitad de la dosis".
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:reduzca|reduce|baje|disminuya|corte|parta|divida)\\s+${ES_SLOT}${med}\\s+(?:a|en|por)\\s+la\\s+mitad`,
"i",
unlessDeferred(
ES_DEFERRAL,
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:reduzca|reduce|baje|disminuya|corte|parta|divida)\\s+${ES_SLOT}${med}\\s+(?:a|en|por)\\s+la\\s+mitad`,
"i",
),
),
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:tome|tómese|inyecte|inyéctese|use)\\s+(?:s[óo]lo\\s+|solamente\\s+)?(?:media\\s+|la\\s+mitad\\s+de\\s+)${ES_SHORT}${med}`,
"i",
unlessDeferred(
ES_DEFERRAL,
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:tome|tómese|inyecte|inyéctese|use)\\s+(?:s[óo]lo\\s+|solamente\\s+)?(?:media\\s+|la\\s+mitad\\s+de\\s+)${ES_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "reduzca la
// dosis.", "aumente la insulina un poco".
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:aumente|suba|incremente|reduzca|baje|disminuya)\\s+${ES_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:ahora|hoy|mañana|esta\\s+noche|un\\s+poco|ligeramente|gradualmente|durante|desde)(?![\\w\\u00C0-\\u024F]))`,
"i",
unlessDeferred(
ES_DEFERRAL,
new RegExp(
`(?<!\\b(?:no|nunca)\\s)\\b(?:aumente|suba|incremente|reduzca|baje|disminuya)\\s+${ES_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:ahora|hoy|mañana|esta\\s+noche|un\\s+poco|ligeramente|gradualmente|durante|desde)(?![\\w\\u00C0-\\u024F]))`,
"i",
),
),
],
dose: [
@@ -8,6 +8,8 @@ import {
WORD_END,
ELIDED,
medicationExtras,
unlessDeferred,
WORD_START,
type LocaleBank,
} from "../shared";
@@ -39,6 +41,9 @@ const RESULT_VERB_FR =
const FR_MED_CLASS =
"(?:anticoagulants?|antiagrégants?|statines?|bêta-?bloquants?|diurétiques?|antidépresseurs?|somnifères?)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const FR_DEFERRAL = `\\?|(?:^|\\s)n['’]|${WORD_START}(?:ne|pas|jamais|sauf|seulement\\s+si|sans|demandez|parlez|médecin\\w*|pharmacien\\w*|docteur|soignant\\w*|spécialiste)${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`${ELIDED}(?:${FR_MED_NOUN}|${medicationExtras(FR_MED_CLASS, names)})`,
@@ -71,19 +76,28 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "réduisez la dose de moitié",
// "prenez une demi-dose", "coupez le comprimé en deux".
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)(?<!n['’])\\b(?:r[ée]duisez|r[ée]duis|diminuez|diminue|baissez|baisse|coupez|coupe|divisez|divise)\\s+${FR_SLOT}${med}\\s+(?:de\\s+moiti[ée]|par\\s+deux|en\\s+deux)`,
"i",
unlessDeferred(
FR_DEFERRAL,
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)(?<!n['’])\\b(?:r[ée]duisez|r[ée]duis|diminuez|diminue|baissez|baisse|coupez|coupe|divisez|divise)\\s+${FR_SLOT}${med}\\s+(?:de\\s+moiti[ée]|par\\s+deux|en\\s+deux)`,
"i",
),
),
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)\\b(?:prenez|prends|injectez|injecte)\\s+(?:seulement\\s+|uniquement\\s+)?(?:une\\s+demi-?\\s?|la\\s+moiti[ée]\\s+d(?:e\\s+|u\\s+|es\\s+|['’]))${FR_SHORT}${med}`,
"i",
unlessDeferred(
FR_DEFERRAL,
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)\\b(?:prenez|prends|injectez|injecte)\\s+(?:seulement\\s+|uniquement\\s+)?(?:une\\s+demi-?\\s?|la\\s+moiti[ée]\\s+d(?:e\\s+|u\\s+|es\\s+|['’]))${FR_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "diminuez votre
// dose.", "augmentez votre insuline un peu".
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)(?<!n['’])\\b(?:augmentez|augmente|diminuez|diminue|r[ée]duisez|r[ée]duis|baissez|baisse)\\s+${FR_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:maintenant|aujourd|demain|ce\\s+soir|un\\s+peu|progressivement|pendant)(?![\\w\\u00C0-\\u024F]))`,
"i",
unlessDeferred(
FR_DEFERRAL,
new RegExp(
`(?<!\\b(?:vous|nous|je|tu|il|elle|on)\\s)(?<!n['’])\\b(?:augmentez|augmente|diminuez|diminue|r[ée]duisez|r[ée]duis|baissez|baisse)\\s+${FR_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:maintenant|aujourd|demain|ce\\s+soir|un\\s+peu|progressivement|pendant)(?![\\w\\u00C0-\\u024F]))`,
"i",
),
),
],
dose: [
@@ -8,6 +8,8 @@ import {
WORD_END,
ELIDED,
medicationExtras,
unlessDeferred,
WORD_START,
type LocaleBank,
} from "../shared";
@@ -36,6 +38,9 @@ const RESULT_VERB_IT =
const IT_MED_CLASS =
"(?:anticoagulant[ei]|antiaggregant[ei]|statin[ae]|betabloccant[ei]|diuretic[oi]|antidepressiv[oi])";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const IT_DEFERRAL = `\\?|${WORD_START}(?:non|mai|senza|solo\\s+se|chieda|chiedi|parli|parla|consult\\w*|medic[oi]|dottor\\w*|farmacist\\w*|specialist\\w*)${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`${ELIDED}(?:${IT_MED_NOUN}|${medicationExtras(IT_MED_CLASS, names)})`,
@@ -63,19 +68,28 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "riduca la dose della metà",
// "prenda mezza dose", "prenda metà della dose".
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:riduca|ridurre|diminuisca|abbassi|tagli|divida)\\s+${IT_SLOT}${med}\\s+(?:della|alla|a|per)\\s+met[àa]`,
"i",
unlessDeferred(
IT_DEFERRAL,
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:riduca|ridurre|diminuisca|abbassi|tagli|divida)\\s+${IT_SLOT}${med}\\s+(?:della|alla|a|per)\\s+met[àa]`,
"i",
),
),
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:prenda|assuma|inietti)\\s+(?:solo\\s+|soltanto\\s+)?(?:mezz[ao]\\s+|(?:la\\s+)?met[àa]\\s+(?:della|del|dell['’]|di)\\s*)${IT_SHORT}${med}`,
"i",
unlessDeferred(
IT_DEFERRAL,
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:prenda|assuma|inietti)\\s+(?:solo\\s+|soltanto\\s+)?(?:mezz[ao]\\s+|(?:la\\s+)?met[àa]\\s+(?:della|del|dell['’]|di)\\s*)${IT_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "riduca la
// dose.", "aumenti l'insulina un po'".
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:aumenti|incrementi|riduca|abbassi|diminuisca)\\s+${IT_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:ora|oggi|domani|stasera|un\\s+po|leggermente|gradualmente|per|da\\s+subito)(?![\\w\\u00C0-\\u024F]))`,
"i",
unlessDeferred(
IT_DEFERRAL,
new RegExp(
`(?<!\\b(?:non|mai)\\s)\\b(?:aumenti|incrementi|riduca|abbassi|diminuisca)\\s+${IT_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:ora|oggi|domani|stasera|un\\s+po|leggermente|gradualmente|per|da\\s+subito)(?![\\w\\u00C0-\\u024F]))`,
"i",
),
),
],
dose: [
@@ -5,6 +5,7 @@ import {
UNIT_END,
ENGINE,
medicationExtras,
unlessDeferred,
type LocaleBank,
} from "../shared";
@@ -50,6 +51,9 @@ const RISK_VERDICT_KO = `(?:은|는|이|가)\\s*[^.?!]{0,10}${RISK_LEVEL_KO}`;
const KO_MED_CLASS =
"(?:혈액\\s*희석제|항응고제|항혈소판제|스타틴|베타\\s*차단제|이뇨제|항우울제)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const KO_DEFERRAL = `\\?|의사|약사|전문의|주치의|상의|라고|지\\s*마`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${KO_MED_NOUN}|(?:${medicationExtras(KO_MED_CLASS, names, false)})\\S{0,3})`,
@@ -74,8 +78,11 @@ export const bank: LocaleBank = {
// "복용량을 늘리세요", "인슐린 용량을 낮추세요". No digit may sit between,
// so "약 2주 동안 걷는 시간을 늘리세요" ("약" = about) passes, and the
// prohibitive "줄이지 마세요" never meets an imperative ending.
new RegExp(
`${med}[^.?!\\d]{0,8}?(?:(?:늘리|올리|높이|줄이|내리|낮추|증량하|감량하)(?:세요|십시오)|(?:늘려|올려|높여|줄여|내려|낮춰)\\s*(?:보세요|주세요|보십시오))`,
unlessDeferred(
KO_DEFERRAL,
new RegExp(
`${med}[^.?!\\d]{0,8}?(?:(?:늘리|올리|높이|줄이|내리|낮추|증량하|감량하)(?:세요|십시오)|(?:늘려|올려|높여|줄여|내려|낮춰)\\s*(?:보세요|주세요|보십시오))`,
),
),
],
// Korean is verb-final: the dose comes first and the change verb closes the
@@ -106,7 +113,12 @@ export const bank: LocaleBank = {
),
// Both a medication object AND a trial cue are required, as in the EN bank,
// so "걸음 수를 두 배로 늘려 2주간 해보세요" never trips.
/(?:약|알|정|인슐린|주사|복용|용량)[^.?!]{0,25}(?:절반|반으로|두\s?배|건너뛰|중단|끊)[^.?!]{0,40}(?:주간|주 동안|일 동안|달 동안|보세요|보십시오|보자|해 ?보)/,
// Number-free too, so it stands down where a pharmacist or doctor is
// asked ("약사에게 … 물어보세요" ends on the same "보세요").
unlessDeferred(
KO_DEFERRAL,
/(?:약|알|정|인슐린|주사|복용|용량)[^.?!]{0,25}(?:절반|반으로|두\s?배|건너뛰|중단|끊)[^.?!]{0,40}(?:주간|주 동안|일 동안|달 동안|보세요|보십시오|보자|해 ?보)/,
),
],
risk: [
new RegExp(`${RISK_NOUN_KO}[^.?!]{0,15}${QUAL_NUM_KO}`, "i"),
@@ -9,6 +9,8 @@ import {
ENGINE,
WORD_START,
medicationExtras,
unlessDeferred,
WORD_END,
type LocaleBank,
} from "../shared";
@@ -50,6 +52,9 @@ const RISK_VERDICT_NL = `(?:is|ligt|lijkt|blijft|wijst\\s+op|komt\\s+uit\\s+op)\
const NL_MED_CLASS =
"\\S*(?:bloedverdunner\\w*|antistolling\\w*|anticoagul\\w*|plaatjesremmer\\w*|statine\\w*|b[eè]tablokker\\w*|plastablet\\w*|diuretic\\w*|diuretica|antidepressiv\\w*|antidepressiva|slaappil\\w*|slaapmiddel\\w*|bloeddrukverlager\\w*|ace-remmer\\w*)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const NL_DEFERRAL = `\\?|${WORD_START}(?:niet|geen|nooit|alleen\\s*,?\\s*(?:als|wanneer|na)|pas\\s*,?\\s*(?:als|na)|zonder|vraag|overleg\\w*|bespreek|arts|artsen|huisarts|apothe\\w*|specialist|dokter|zorgverlener\\w*)${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${NL_MED_NOUN}|${medicationExtras(NL_MED_CLASS, names)})`,
@@ -113,19 +118,28 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "verlaag je dosis met de helft",
// "neem een halve dosis", "snijd de tablet doormidden".
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:verlaag|verminder|deel|breek|snijd)\\s+${NL_SLOT}${med}\\s+(?:met\\s+de\\s+helft|door\\s+(?:twee|2)|doormidden|in\\s+twee)`,
"i",
unlessDeferred(
NL_DEFERRAL,
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:verlaag|verminder|deel|breek|snijd)\\s+${NL_SLOT}${med}\\s+(?:met\\s+de\\s+helft|door\\s+(?:twee|2)|doormidden|in\\s+twee)`,
"i",
),
),
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:neem|slik|spuit|injecteer)\\s+(?:alleen\\s+|maar\\s+)?(?:een\\s+halve|de\\s+helft\\s+van)\\s+${NL_SHORT}${med}`,
"i",
unlessDeferred(
NL_DEFERRAL,
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:neem|slik|spuit|injecteer)\\s+(?:alleen\\s+|maar\\s+)?(?:een\\s+halve|de\\s+helft\\s+van)\\s+${NL_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "verlaag je
// dosis.", "verhoog je insuline wat". "Verlaag je dosis niet" is a warning.
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:verhoog|verlaag|verminder)\\s+${NL_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:nu|vandaag|morgen|vanavond|iets|wat|een\\s+beetje|geleidelijk|vanaf)\\b)`,
"i",
unlessDeferred(
NL_DEFERRAL,
new RegExp(
`(?<!\\b(?:niet|nooit)\\s)\\b(?:verhoog|verlaag|verminder)\\s+${NL_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:nu|vandaag|morgen|vanavond|iets|wat|een\\s+beetje|geleidelijk|vanaf)\\b)`,
"i",
),
),
],
dose: [
@@ -6,6 +6,9 @@ import {
TARGET,
ENGINE,
medicationExtras,
unlessDeferred,
WORD_START,
WORD_END,
type LocaleBank,
} from "../shared";
@@ -34,6 +37,9 @@ const RESULT_VERB_PL =
const PL_MED_CLASS =
"(?:lek\\S*\\s+przeciwzakrzepow\\S*|antykoagulant\\S*|statyn\\S*|beta-?bloker\\S*|diuretyk\\S*|lek\\S*\\s+moczopędn\\S*|antydepresant\\S*)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const PL_DEFERRAL = `\\?|${WORD_START}(?:nie|bez|tylko\\s+(?:jeśli|gdy|jeżeli)|zapytaj|skonsultuj|porozmawiaj|lekarz\\w*|farmaceut\\w*|aptek\\w*|specjalist\\w*)${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${PL_MED_NOUN}|${medicationExtras(PL_MED_CLASS, names)})`,
@@ -61,20 +67,29 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "zmniejsz dawkę o połowę",
// "weź połowę dawki", "przyjmij pół dawki".
new RegExp(
`(?<!\\bnie\\s)\\b(?:zmniejsz|obni[żz]|zredukuj|podziel|przetnij|przełam)(?:cie)?\\s+${PL_SLOT}${med}\\s+(?:o\\s+połowę|na\\s+pół|na\\s+połowę|na\\s+dwie\\s+części)`,
"i",
unlessDeferred(
PL_DEFERRAL,
new RegExp(
`(?<!\\bnie\\s)\\b(?:zmniejsz|obni[żz]|zredukuj|podziel|przetnij|przełam)(?:cie)?\\s+${PL_SLOT}${med}\\s+(?:o\\s+połowę|na\\s+pół|na\\s+połowę|na\\s+dwie\\s+części)`,
"i",
),
),
new RegExp(
`(?:(?<!\\bnie\\s)\\b(?:we[źz]|bierz|przyjmij|przyjmuj|wstrzyknij)|\\b(?:proszę|należy)\\s+(?:wzi[ąa][ćc]|przyj[ąa][ćc]|przyjmowa[ćc]))\\s+(?:tylko\\s+)?(?:pół|połowę|połowy)\\s+${PL_SHORT}${med}`,
"i",
unlessDeferred(
PL_DEFERRAL,
new RegExp(
`(?:(?<!\\bnie\\s)\\b(?:we[źz]|bierz|przyjmij|przyjmuj|wstrzyknij)|\\b(?:proszę|należy)\\s+(?:wzi[ąa][ćc]|przyj[ąa][ćc]|przyjmowa[ćc]))\\s+(?:tylko\\s+)?(?:pół|połowę|połowy)\\s+${PL_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "zmniejsz
// dawkę.", "zwiększ dawkę insuliny.". The noun closes the clause, or one
// genitive word does ("dawkę insuliny"), or a time or degree word follows.
new RegExp(
`(?<!\\bnie\\s)\\b(?:zwi[ęe]ksz|zmniejsz|obni[żz]|podnie[śs]|podwy[żz]sz|zredukuj)(?:cie)?\\s+${PL_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+[^\\s\\d.?!,;:]+\\s*(?:[.!?;:)]|$)|\\s+(?:teraz|dziś|dzisiaj|jutro|trochę|nieco|stopniowo|od\\s+jutra|przez)(?=[\\s.,;:!?]|$))`,
"i",
unlessDeferred(
PL_DEFERRAL,
new RegExp(
`(?<!\\bnie\\s)\\b(?:zwi[ęe]ksz|zmniejsz|obni[żz]|podnie[śs]|podwy[żz]sz|zredukuj)(?:cie)?\\s+${PL_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?!(?:kofein|cukr|cukier|ruch|sol|sól|alkohol|kalori|białk|wod|kaw|herbat|tłuszcz|węglowodan|aktywnoś|ćwicze|snu|sen|błonnik)[^\\s]*\\s*(?:[.!?;:)]|$))[^\\s\\d.?!,;:]+\\s*(?:[.!?;:)]|$)|\\s+(?:teraz|dziś|dzisiaj|jutro|trochę|nieco|stopniowo|od\\s+jutra|przez)(?=[\\s.,;:!?]|$))`,
"i",
),
),
],
dose: [
@@ -8,6 +8,9 @@ import {
LETTER,
ENGINE,
medicationExtras,
unlessDeferred,
WORD_START,
WORD_END,
type LocaleBank,
} from "../shared";
@@ -50,6 +53,9 @@ const RISK_VERDICT_PT = `(?:é|est[áa]|parece|fica|continua|indica|mostra|apont
const PT_MED_CLASS =
"(?:anticoagulantes?|antiagregantes?|estatinas?|beta-?bloqueadores?|diur[ée]ticos?|antidepressivos?|anti-?hipertensivos?|sonífer[oa]s?|rem[ée]dios?\\s+(?:para\\s+(?:a\\s+)?|de\\s+)(?:press[ãa]o|dormir|afinar\\s+o\\s+sangue))";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const PT_DEFERRAL = `\\?|${WORD_START}(?:não|nao|nunca|sem|só\\s+se|somente\\s+se|antes\\s+(?:de\\s+)?que|até\\s+que|depois\\s+que|pergunte|converse|fale\\s+com|consult\\w*|médic[oa]s?|doutor[a]?|farmacêutic[oa])${WORD_END}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${PT_MED_NOUN}|${medicationExtras(PT_MED_CLASS, names)})`,
@@ -94,19 +100,28 @@ export const bank: LocaleBank = {
),
// Halving or a fraction with no number: "reduza a dose pela metade",
// "tome meia dose", "corte o comprimido ao meio".
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:reduza|diminua|baixe|corte|parta|divida)\\s+${PT_SLOT}${med}\\s+(?:pela\\s+metade|ao\\s+meio|em\\s+duas\\s+partes|[àa]\\s+metade)`,
"i",
unlessDeferred(
PT_DEFERRAL,
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:reduza|diminua|baixe|corte|parta|divida)\\s+${PT_SLOT}${med}\\s+(?:pela\\s+metade|ao\\s+meio|em\\s+duas\\s+partes|[àa]\\s+metade)`,
"i",
),
),
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:tome|use|aplique|injete)\\s+(?:s[óo]\\s+|apenas\\s+|somente\\s+)?(?:meia\\s+|(?:a\\s+)?metade\\s+d[aoe]s?\\s+)${PT_SHORT}${med}`,
"i",
unlessDeferred(
PT_DEFERRAL,
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:tome|use|aplique|injete)\\s+(?:s[óo]\\s+|apenas\\s+|somente\\s+)?(?:meia\\s+|(?:a\\s+)?metade\\s+d[aoe]s?\\s+)${PT_SHORT}${med}`,
"i",
),
),
// A bare change imperative on a medication, no target: "reduza a
// dose.", "aumente a insulina um pouco".
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:aumente|suba|eleve|reduza|diminua|baixe|abaixe)\\s+(?:(?:a|o|as|os)\\s+)?${PT_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:agora|hoje|amanh[ãa]|um\\s+pouco|aos\\s+poucos|gradualmente|a\\s+partir)(?![\\w\\u00C0-\\u024F]))`,
"i",
unlessDeferred(
PT_DEFERRAL,
new RegExp(
`(?<!\\b(?:não|nao|nunca|jamais)\\s)\\b(?:aumente|suba|eleve|reduza|diminua|baixe|abaixe)\\s+(?:(?:a|o|as|os)\\s+)?${PT_SHORT}${med}(?=\\s*(?:[.!?;:)]|$)|\\s+(?:agora|hoje|amanh[ãa]|um\\s+pouco|aos\\s+poucos|gradualmente|a\\s+partir)(?![\\w\\u00C0-\\u024F]))`,
"i",
),
),
],
dose: [
@@ -146,3 +146,22 @@ export interface LocaleBank {
/** A change stem that cancels that exemption. */
changeStem: RegExp;
}
/**
* The number-free change shapes (halve it, take half, a bare "reduce your
* dose") have no unit to anchor them, so they stand down in a sentence that
* defers or declines: a question, a negation anywhere in it (a trailing
* "heute nicht" or "vandaag niet" came after the time word the pattern
* stops at), a restrictive condition ("only if", "nur wenn"), or a
* clinician named in it ("if your doctor agrees", "ask your pharmacist").
* `deferral` is the locale's alternation of those signals; it is checked
* over the whole sentence, which the screen hands in one at a time. The
* shapes with a stated amount do not stand down: a target dose stays
* blocked whatever surrounds it.
*/
export function unlessDeferred(deferral: string, pattern: RegExp): RegExp {
return new RegExp(
`^(?![\\s\\S]*(?:${deferral}))[\\s\\S]*?(?:${pattern.source})`,
pattern.flags,
);
}
+4 -3
View File
@@ -99,9 +99,10 @@ const US: CrisisResourceSet = {
};
/**
* Brazil (an explicit region signal: a bare "br" or "pt_BR"-style tag a
* native client sends from the device region) — CVV by phone and chat, SAMU
* as the emergency number. Both numbers are free and answer around the clock
* Brazil, reached only by the bare region code "br". "pt-BR" and "pt_BR"
* are not a region signal: the web folds every Portuguese reader into that
* one bundle, so they get PT_BR_AMBIGUOUS. CVV by phone and chat, SAMU as
* the emergency number. Both numbers are free and answer around the clock
* (cvv.org.br; the Ministry of Health's SAMU 192 page).
*/
const BR: CrisisResourceSet = {