import { INGREDIENTS, INGREDIENT_BY_KEY, PRODUCTS } from './catalog.js';
import { normalizeText } from './normalize.js';
import { SAME_CLAUSE } from './rules.js';
/**
* J1: paracetamol is never a regimen above the most anyone should take in 24
* hours, whatever the schedule. Over several days that is a staggered
* overdose, which needs same-day hospital assessment even when the person
* feels well: the delay is what makes it dangerous. The limits are 8 tablets
* a day (8 x 500 mg is 4,000 mg) and 4 doses a day of a children's product
* (and never more than 8 of anything). Pharmacist to confirm the threshold of
* 8: US regular-strength tablets are 325 mg.
*/
export const PARACETAMOL_TABLETS_A_DAY = 8;
export const CHILD_PARACETAMOL_DOSES_A_DAY = 4;
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const paracetamol = INGREDIENT_BY_KEY.get('paracetamol');
/**
* Paracetamol by any name: its generic names, every brand or combination the
* catalogue knows with it in (Panadol, Calpol, co-codamol, Night Nurse...),
* and how people write them ("Emzor" for Emzor paracetamol, "paras").
*/
const NAMES = [...new Set([paracetamol?.name ?? 'Paracetamol', ...(paracetamol?.aliases ?? []),
...PRODUCTS.filter((product) => product.ingredients.includes('paracetamol')).flatMap((product) => product.names),
'paracetemol', 'paracetamole', 'paracetamal', 'parcetamol', 'paracetmol', 'paras', 'emzor', 'panadol baby', 'calpol six plus', 'calpol infant',
].map((name) => normalizeText(name)))].sort((a, b) => b.length - a.length);
const NAME = `(?:${NAMES.map(escapeRegex).join('|')})s?`;
const SMALL: Record<string, number> = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10, eleven: 11,
twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19 };
const TENS: Record<string, number> = { twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90 };
const DIGIT_WORDS = 'one|two|three|four|five|six|seven|eight|nine';
const NUM = `(?:[0-9]+|(?:${Object.keys(TENS).join('|')})(?: (?:${DIGIT_WORDS}))?|${Object.keys(SMALL).join('|')}|a dozen|dozen|a hundred|hundred|a couple of|a few|several)`;
/** "12", "twelve", "twenty four", "a dozen"; "a few" and "several" are read as 3. */
function valueOf(words: string): number {
if (/^[0-9]+$/.test(words)) return Number(words);
if (words === 'a dozen' || words === 'dozen') return 12;
if (words === 'a hundred' || words === 'hundred') return 100;
if (words === 'a couple of') return 2;
if (words === 'a few' || words === 'several') return 3;
const [first, second] = words.split(' ');
return (TENS[first] ?? SMALL[first] ?? 0) + (second ? SMALL[second] ?? 0 : 0);
}
const TABLET_UNIT = '(?:tablets?|tabs?|caplets?|capsules?|caps|pills?)';
const DOSE_UNIT = '(?:doses?|sachets?|spoons?|spoonfuls?|teaspoons?|teaspoonfuls?|syringes?|syringefuls?|measures?)';
const UNIT = `(?:${TABLET_UNIT}|${DOSE_UNIT})`;
/** L4: a form after the name or the strength: "paracetamol syrup 10 ml", "Panadol 500mg tablets". */
const FORM = `(?:${UNIT}|syrup|suspension|elixir|liquid|solution|soluble|oral|effervescent|dispersible|drops)`;
const QUALIFIER = '(?:about|around|over|like|almost|nearly|up to|maybe|at least|more than|another|only|just)';
const OWNER = '(?:my|her|his|their|the|your|our|dis|di|these|those)';
/**
* H1: a strength or a volume is never a count: "500 mg", "500mg", "1 g", "5 ml",
* "120mg/5ml". "Panadol 500 mg twice a day" is 2 doses, not 500 tablets.
*/
const STRENGTH_UNIT = '(?:mg|mgs|milligrams?|g|gm|gms|grams?|mcg|ug|micrograms?|ml|mls|millilitres?|milliliters?|cc)';
const STRENGTH = `(?:[0-9]+(?:\\.[0-9]+)?|${NUM}) ?${STRENGTH_UNIT}(?: ?/ ?(?:[0-9]+(?:\\.[0-9]+)? ?)?${STRENGTH_UNIT})?(?![a-z0-9])`;
const NOT_A_STRENGTH = `(?!(?:\\.[0-9]+)? ?${STRENGTH_UNIT}(?![a-z0-9]))`;
/**
* M-3: a strength with no unit, right after a paracetamol name: the usual
* strengths ("Panadol 500 twice a day", "Tylenol 325") and pairs
* ("co-codamol 30/500", "8/500", "500/65"). Skipped like "500 mg", and never
* a count: 120 tablets or more is never a dose (the table's "fifty or more"
* still reads them as too much).
*/
const BARE_STRENGTH = '(?:120|125|160|250|325|500|650|665|1000|[0-9]+(?:\\.[0-9]+)?/[0-9]+(?:\\.[0-9]+)?)(?![a-z0-9./])';
/** "10 paracetamol", "12 tablets of Panadol", "10 of her Panadol", "10 Calpol sachets", "6 doses of Calpol", "two 500mg paracetamol". */
const COUNT_FIRST = new RegExp(`(?<![a-z0-9.])(?!${BARE_STRENGTH})(?<count>${NUM})${NOT_A_STRENGTH}(?: ?x)?(?: ${STRENGTH})?(?: (?<unit>${UNIT}))?(?: of)?(?: ${OWNER})?(?: ${STRENGTH})? (?<name>${NAME})`
// LOW: a strength after the name ("I take 3 co-codamol 30/500 four times a day").
+ `(?: (?:${BARE_STRENGTH}|${STRENGTH}))?(?: (?<after>${UNIT}))?(?![a-z0-9])`, 'g');
/** "paracetamol 10 a day", "Panadol, 3 tablets 4 times a day", "paracetamol 500 mg, 3 tablets 4 times a day" (the strength skipped). */
const NAME_FIRST = new RegExp(`(?<![a-z0-9])(?<name>${NAME})(?: (?<after>${FORM}))?(?: ${BARE_STRENGTH})?(?: ${STRENGTH}){0,2}(?: (?<form>${FORM}))?(?: ${QUALIFIER})? (?<x>x ?)?(?!${BARE_STRENGTH})(?<count>${NUM})${NOT_A_STRENGTH}`
+ `(?: (?<unit>${UNIT}))?(?! times)(?![a-z0-9])`, 'g');
/** "Calpol 6 times a day", "Calpol 5 ml four times a day", R9-L4: "paracetamol 1g every 3 hours": doses, with no count. */
const NAME_TIMES = new RegExp(`(?<![a-z0-9])(?<name>${NAME})(?: (?<after>${FORM}))?(?: ${BARE_STRENGTH})?(?: ${STRENGTH}){0,2}(?: (?<form>${FORM}))?(?: ${QUALIFIER})?`
+ `(?= (?:twice|thrice|${NUM} times?|x ?${NUM}|${NUM} ?x|every ${NUM} (?:hours|hrs))(?![a-z0-9]))`, 'g');
/**
* L4: a total in mg or g: "I took 8000mg of paracetamol today", "she swallowed 15 g of Panadol". With "of", or over 1000 mg, it
* is a total; "500mg paracetamol" is a strength, and the count after it is read instead.
*/
const MASS_FIRST = new RegExp(`(?<![a-z0-9.])(?<mass>[0-9]+(?:\\.[0-9]+)?) ?(?<massUnit>mg|mgs|milligrams?|g|gm|gms|grams?)(?<of> of)?(?: ${OWNER})? (?<name>${NAME})(?![a-z0-9])`, 'g');
/** A clock time, or a time of day: "at 8am", "at 10 30pm", "at 2", "at noon", "at bedtime". */
const CLOCK = '(?:[0-9]{1,2}(?: ?[0-9]{2})?(?: ?(?:am|pm|oclock))?|noon|midday|midnight|night|bedtime|lunch|lunchtime|breakfast|dinner|teatime)';
/**
* LOW, M-E: amounts at clock times, one day's worth: "paracetamol 2 g at 8am, 2 g at 2pm and 1 g at 8pm" is 5 g, "I took 2 g
* of paracetamol at 8am and 2 g at 2pm and 1 g at 8pm" too, and "4 paracetamol at 10pm last night and 6 at 6am" is 10 tablets.
* R9-L4: a child's doses by volume ("Calpol 5ml at 8am, 5ml at 12, ...") are doses. A count is never a date or a room ("on the 12
* at 3pm").
*/
const AT_A_TIME = new RegExp(`(?<![a-z0-9.])(?:(?<mass>[0-9]+(?:\\.[0-9]+)?) ?(?<massUnit>mg|mgs|milligrams?|g|gm|gms|grams?)`
+ `|(?<volume>[0-9]+(?:\\.[0-9]+)?) ?(?:ml|mls|millilitres?|milliliters?)`
+ `|(?<!(?:the|on|of|from|until|till|by|before|after|since|at|room|ward|bed|bus|number|no) )(?!${BARE_STRENGTH})(?<count>${NUM})${NOT_A_STRENGTH}(?: more)?(?: ${TABLET_UNIT})?)`
+ `(?:(?: of)?(?: ${OWNER})? (?<name>${NAME})(?: ${TABLET_UNIT})?)? at ${CLOCK}(?![a-z0-9])`, 'g');
/**
* M-E: every other medicine the catalogue knows, by any name, so an amount at a clock time is read with the medicine named
* nearest before it: "metformin 1g at 8am and 1g at 8pm" is metformin, never paracetamol.
*/
const OTHER_NAMES = [...new Set([...INGREDIENTS.flatMap((ingredient) => [ingredient.name, ...(ingredient.aliases ?? [])]), ...PRODUCTS.flatMap((product) => product.names)]
.map((name) => normalizeText(name)).filter((name) => name && !NAMES.includes(name)))].sort((a, b) => b.length - a.length);
/**
* R9-M6: what may sit between an amount at a clock time and the paracetamol name it is read with: more amounts and times, and
* joining words ("paracetamol 1g at 8am, then 1g at 2pm and another 1g at 8pm"). Anything else, such as a medicine the catalogue
* doesn't know ("gabapentin 300mg at 8am"), ends the run.
*/
const RUN = /^(?:\s|[0-9.]|mg|mgs|ml|mls|g|gm|grams?|tablets?|tabs?|at|am|pm|oclock|noon|midday|midnight|night|bedtime|lunch|lunchtime|breakfast|dinner|teatime|and|then|plus|another|more|also|of|my|her|his|last|this|morning|evening|today|tonight)+$/;
/** R9-M6: "I take", "Mum takes" before a list of amounts that opens the text. */
const LIST_OPENING = /^(?:(?:i|she|he|we|they|mum|mom|dad|my [a-z]+) (?:take|takes|took|had|have|has|gave|give|gives) )/;
const MEDICINE_NAMED = new RegExp(`(?<![a-z0-9])(?:(?<paracetamol>${NAME})|(?:${OTHER_NAMES.map(escapeRegex).join('|')})s?)(?![a-z0-9])`, 'g');
/** LOW: one dose in mg or g, written with the name ("paracetamol 1g x 5 a day"); never a pair ("30/500") or a volume ("120mg/5ml"). */
const DOSE_MASS = /(?<![a-z0-9./])([0-9]+(?:\.[0-9]+)?) ?(mg|mgs|milligrams?|g|gm|gms|grams?)(?![a-z0-9./])/g;
function doseMg(written: string): number | null {
if (written.includes('/')) return null;
const masses = [...written.matchAll(DOSE_MASS)];
return masses.length === 1 ? Number(masses[0][1]) * (/^(?:g|gm|gms|grams?)$/.test(masses[0][2]) ? 1000 : 1) : null;
}
/** The most paracetamol in 24 hours, in mg. */
const PARACETAMOL_MG_A_DAY = paracetamol?.maxDailyMg ?? 4000;
/** One day: "a day", "daily", and M3: "today", "since this morning", "in one day", "yesterday". */
const ONE_DAY = '(?:(?:a|per|each|every|in a|in one|one|in the) day|everyday|daily|today|so far today|since (?:this )?(?:morning|afternoon|breakfast|lunch)'
+ '|this (?:morning|afternoon|evening)|tonight|last night|yesterday|(?:in|per|every|within) (?:24|twenty four) (?:hours|hrs)'
+ '|in the (?:last|past) (?:24|twenty four) (?:hours|hrs)|a 24 hour period)';
/** How often: once a day, a number of times, or a total over days. */
const FREQUENCY_WORDS = `(?:${[
`(?<perDay>${ONE_DAY})`,
// R9-L4: "at once" or "in one go" is one day's too ("I took 9 paracetamol at once at 2pm").
'(?<once>in the (?:morning|evening|afternoon)|at (?:night|bedtime|lunch|lunchtime|noon|midday|teatime|tea time|dinner|supper|breakfast)|(?:every|each) (?:morning|evening|night|afternoon)|nightly|once (?:a|per|each|every) day|once daily|(?:all )?at once|in one go|at one time|at a go)',
`(?<twice>twice|thrice) ${ONE_DAY}`,
`(?<times>${NUM}) times ${ONE_DAY}`,
// L4: "x 3", "x3 a day", "3x daily".
`x ?(?<timesX>${NUM})(?: (?:times? )?${ONE_DAY})?|(?<xTimes>${NUM}) ?x ${ONE_DAY}`,
'(?<morningAndNight>morning and (?:evening|night))',
`(?:over|in|across|within)(?: the (?:last|past|next))? (?<overDays>${NUM}) days`,
`(?:over|in|across|within)(?: the (?:last|past|next))? (?<overWeeks>a|one|${NUM}) weeks?`,
`(?:over|in|within)(?: the (?:last|past|next))? (?<overHours>${NUM}) (?:hours|hrs)`,
`(?<forTime>for(?: the (?:last|past|next))? (?:${NUM}) (?:days|weeks)|for a week)`,
'(?<perWeek>a week|per week|every week|weekly)',
`every (?<everyHours>${NUM}) (?:hours|hrs)`,
].join('|')})(?![a-z0-9])`;
/** How often, soon after the count and in the same clause. */
const FREQUENCY = new RegExp(`^${SAME_CLAUSE}{0,4}? ${FREQUENCY_WORDS}`);
/** How often, straight after a count that adds to one ("... and 1 at night"). */
const FREQUENCY_NEXT = new RegExp(`^ ${FREQUENCY_WORDS}`);
/**
* LOW: more of the same paracetamol in the same sentence: "2 four times a day and 1 at night", "2 at lunch, 3 at night", and
* (L3) a child's extra dose by volume: "Calpol 5ml 4 times a day and another 5ml at night".
*/
const MORE = new RegExp(`^(?: (?:and|plus|then|also|with|or|but))?(?: (?:another|an extra|extra))? (?:(?<count>${NUM}|once)${NOT_A_STRENGTH}(?: (?<unit>${UNIT}))?|(?<volume>${STRENGTH}))`
+ '(?: more)?(?: of (?:them|those|it|these))?(?= )');
/**
* L1, H-1.5: the day a schedule names, so "6 yesterday and 6 today" is two days of 6, not 12; null for any day ("a day", "at
* night"). "Last night", "tonight", "this morning", "today" and "since" are one rolling 24 hours ("5 last night and 5 this
* morning" is 10); only "yesterday" is a day of its own.
*/
type Day = 'yesterday' | 'today' | null;
const dayOf = (schedule: string): Day => (/(?<![a-z])yesterday(?![a-z])/.test(schedule) ? 'yesterday'
: /(?<![a-z])(?:today|tonight|last night|this (?:morning|afternoon|evening)|since)(?![a-z])/.test(schedule) ? 'today' : null);
/** How many units and doses a day a count comes to, which day, and how long the schedule is, or null with no schedule. */
function perDay(count: number | null, rest: string, frequency = FREQUENCY): { units: number | null; doses: number | null; length: number; day: Day } | null {
const found = frequency.exec(rest);
const groups = found?.groups;
if (!found || !groups) return null;
const length = found[0].length;
const day = dayOf(found[0]);
// "Every 3 hours" over a waking day of 16 hours: 5 doses.
const timesWritten = groups.times ?? groups.timesX ?? groups.xTimes;
const times = groups.twice ? (groups.twice === 'thrice' ? 3 : 2) : timesWritten ? valueOf(timesWritten) : groups.morningAndNight ? 2
: groups.everyHours ? Math.max(1, Math.floor(16 / Math.max(valueOf(groups.everyHours), 1))) : null;
if (times !== null) return { units: count === null ? null : count * times, doses: times, length, day };
if (count === null) return null;
const days = groups.overDays ? valueOf(groups.overDays) : groups.overWeeks ? 7 * (/^(?:a|one)$/.test(groups.overWeeks) ? 1 : valueOf(groups.overWeeks))
: groups.overHours ? Math.max(1, valueOf(groups.overHours) / 24) : groups.perWeek ? 7 : 1;
// "12 a day", and "10 for 3 days" read as 10 a day: the more dangerous reading.
return { units: count / Math.max(days, 1), doses: count / Math.max(days, 1), length, day };
}
/** A children's product, or a child: up to 4 doses a day. */
const CHILD = /(?<![a-z0-9])(?:calpol|panadol baby|babys?|babies|infants?|child|childs|children|childrens|kids?|toddlers?|pikin|paediatric|pediatric|junior|syrup|suspension|elixir|sachets?|spoons?|spoonfuls?|teaspoons?|syringes?|months? old)(?![a-z0-9])/;
const YOUNG = new RegExp(`(?<![a-z0-9])(?<age>${NUM}) (?:year|yr)s? old(?![a-z0-9])`);
const isChild = (clause: string) => CHILD.test(clause) || (YOUNG.exec(clause)?.groups ? valueOf(YOUNG.exec(clause)!.groups!.age) < 16 : false);
/**
* Asked about doing it, not done: "Can I take 10 paracetamol a day?", "Is it
* ok to take...", "If I take...", "Is 10 a day too much?". Not when it is
* being taken: "I take 12 a day", "Can she keep taking 10 a day?", "If she
* has been taking...".
*/
// L2: a child's age may sit on either side of the verb: "Can my 5 year old have...", "Can I give my 3 year old Calpol...".
const ASKED_VERB = /(?:^| )(?:can|could|should|shall|may|might|would|will|to|if|fit|make|do)((?: [a-z0-9]+){0,4}) (?:take|have|give|use|swallow|drink|eat|taking|having|giving|using)(?: [a-z0-9]+){0,4} $/;
const ALREADY = /(?:^| )(?:is|are|am|was|were|been|be|has|had|have|did|dey|don|keep|keeps|continue|continues|still|carry|stop)(?= |$)/;
const ASKED_NO_VERB = /^(?:is|isnt|are|arent|would|will|wouldnt|can|could|how about|what about)(?: [a-z]+){0,3} $/;
const TAKEN_WORD = /(?:^| )(?:take|takes|took|taken|taking|give|gives|gave|given|giving|swallow|swallowed|had|has|been|used|using)(?= |$)/;
function asked(before: string): boolean {
const verb = ASKED_VERB.exec(before);
if (verb) return !ALREADY.test(verb[1]);
return ASKED_NO_VERB.test(before) && !TAKEN_WORD.test(before);
}
const DOSE_UNIT_ONLY = new RegExp(`^${DOSE_UNIT}$`);
/**
* J1: paracetamol over the most in 24 hours, in symptom text or a question:
* more than 8 tablets a day, more than 4 doses a day of a children's product,
* or a total over several days that averages more than 8 a day ("took 30 over
* 3 days"). Everything a sentence says about one day is added up (LOW): "2
* four times a day and 1 at night" is 9, but "6 yesterday and 6 today" is 6
* a day (L1). "taken" when it was or is being taken ("took", "has
* been taking", "takes", "gave her"), "asked" when the question is about doing
* it, null when neither. Taken wins when a text says both.
*/
export function paracetamolOverLimit(text: string | null | undefined): 'taken' | 'asked' | null {
let found: 'taken' | 'asked' | null = null;
// LOW, M-E: amounts at clock times, wherever they are in the text, are one day's paracetamol. R9-M6: each one only with the
// paracetamol name in it, or as the nearest name before it with nothing but amounts, times and joining words in between; or,
// for a list that opens the text ("1g at 8am, 1g at 2pm ... of paracetamol"), the first name after it, the same way.
const whole = normalizeText(text ?? '');
const named = [...whole.matchAll(MEDICINE_NAMED)];
const ofParacetamol = (at: RegExpMatchArray) => {
if (at.groups!.name) return true;
const before = named.filter((name) => name.index! + name[0].length <= at.index!).pop();
if (before) return Boolean(before.groups?.paracetamol) && RUN.test(whole.slice(before.index! + before[0].length, at.index!) || ' ');
const after = named.find((name) => name.index! >= at.index!);
const opening = whole.slice(0, at.index!).replace(LIST_OPENING, '');
return (!opening.trim() || RUN.test(opening)) && Boolean(after?.groups?.paracetamol) && RUN.test(whole.slice(at.index!, after!.index!) || ' ');
};
const atTimes = [...whole.matchAll(AT_A_TIME)].filter(ofParacetamol);
const atTimesMg = atTimes.reduce((sum, at) => sum + (at.groups!.mass ? Number(at.groups!.mass) * (/^(?:g|gm|gms|grams?)$/.test(at.groups!.massUnit) ? 1000 : 1) : 0), 0);
const atTimesTablets = atTimes.reduce((sum, at) => sum + (at.groups!.count ? valueOf(at.groups!.count) : 0), 0);
const atTimesChildDoses = isChild(whole) ? atTimes.filter((at) => at.groups!.volume).length : 0;
if (atTimesMg > PARACETAMOL_MG_A_DAY || atTimesTablets > PARACETAMOL_TABLETS_A_DAY || atTimesChildDoses > CHILD_PARACETAMOL_DOSES_A_DAY) {
if (!asked(whole.slice(0, atTimes[0].index!))) return 'taken';
found = 'asked';
}
// Sentences end at "." only before a space, so "2.5 ml" stays whole.
for (const raw of (text ?? '').split(/(?:[!?;\n]|\.(?=\s|$))+/)) {
const clause = normalizeText(raw);
if (!clause) continue;
const child = isChild(clause);
// In the order written, each once: "Calpol 5 ml four times a day" is one dose statement, not two.
const matches = [...clause.matchAll(COUNT_FIRST), ...clause.matchAll(NAME_FIRST), ...clause.matchAll(NAME_TIMES), ...clause.matchAll(MASS_FIRST)]
.sort((a, b) => a.index! - b.index!);
// L1: what each day comes to; "daily" is any day ("a day", "at night"), and counts towards today and yesterday too.
const units = { daily: 0, today: 0, yesterday: 0 };
const childDoses = { daily: 0, today: 0, yesterday: 0 };
const mg = { daily: 0, today: 0, yesterday: 0 };
let end = 0;
let taken = false;
let counted = false;
for (const match of matches) {
if (match.index! < end) continue;
const groups = match.groups!;
if (groups.mass) {
// L4: a total in mg ("8000mg of paracetamol today"); with no schedule, taken at once.
const total = Number(groups.mass) * (/^(?:g|gm|gms|grams?)$/.test(groups.massUnit) ? 1000 : 1);
const at = match.index! + match[0].length;
if (!groups.of && total <= 1000) {
// LOW: a dose so many times a day is the dose times the doses: "1g paracetamol 6 times a day" is 6 g.
const doses = perDay(null, clause.slice(at), FREQUENCY_NEXT);
if (!doses?.doses) continue;
counted = true;
taken ||= !asked(clause.slice(0, match.index!));
mg[doses.day ?? 'daily'] += total * doses.doses;
end = at + doses.length;
continue;
}
const day = perDay(total, clause.slice(at)) ?? { units: total, doses: null, length: 0, day: null };
counted = true;
taken ||= !asked(clause.slice(0, match.index!));
mg[day.day ?? 'daily'] += day.units ?? 0;
end = at + day.length;
continue;
}
const childProduct = child || /calpol|baby|infant/.test(groups.name);
// An adult's doses ("6 doses a day") aren't counted: only tablets, or a bare count ("12 Panadol a day").
if (!childProduct && DOSE_UNIT_ONLY.test(groups.unit ?? groups.after ?? groups.form ?? '')) continue;
let at = match.index! + match[0].length;
const day = perDay(groups.count ? valueOf(groups.count) : null, clause.slice(at));
if (!day) continue;
counted = true;
taken ||= !asked(clause.slice(0, match.index!));
const statementDay = day.day ?? 'daily';
units[statementDay] += day.units ?? 0;
if (childProduct) childDoses[statementDay] += day.doses ?? 0;
// LOW: a dose in mg, with no tablets, so many times a day: "paracetamol 1g 6 times a day" and "paracetamol 1g x 5 a day" are
// 6 g and 5 g. Never with tablets ("paracetamol 1g, 2 tablets 4 times a day" can be 2 x 500 mg).
const dose = groups.unit || (groups.count && !groups.x) ? null : doseMg(match[0]);
if (dose !== null) mg[statementDay] += dose * ((groups.count ? day.units : day.doses) ?? 0);
at += day.length;
// More of the same, straight after: "... and 1 at night" (the same day), "... and 6 today" (its own day).
for (let more = MORE.exec(clause.slice(at)); more; more = MORE.exec(clause.slice(at))) {
// A volume ("another 5ml") is one dose, and never tablets.
const volume = Boolean(more.groups!.volume);
const next = perDay(volume || more.groups!.count === 'once' ? 1 : valueOf(more.groups!.count), clause.slice(at + more[0].length), FREQUENCY_NEXT);
if (!next) break;
const nextDay = next.day ?? statementDay;
if (!volume) units[nextDay] += next.units ?? 0;
if (childProduct) childDoses[nextDay] += next.doses ?? 0;
at += more[0].length + next.length;
}
end = at;
}
const most = (totals: { daily: number; today: number; yesterday: number }) => totals.daily + Math.max(totals.today, totals.yesterday);
if (!counted || (most(units) <= PARACETAMOL_TABLETS_A_DAY && most(childDoses) <= CHILD_PARACETAMOL_DOSES_A_DAY && most(mg) <= PARACETAMOL_MG_A_DAY)) continue;
if (taken) return 'taken';
found = 'asked';
}
return found;
}