object with classifier regex and labeled regex
export const buildGradeRegexes = (): GradeRegexes => {
// cases("grade") -> "(?:grade|Grade|grade|GRADE)"
// Case aware character classes, so words can be upper, lower or ucfirst.
// flag (which older V8 builds reject).
// const ci = (word: string) =>
// [...word]
// .map((c) => (/[a-zA-Z]/i.test(c) ? `[${c.toUpperCase()}${c.toLowerCase()}]` : c))
// .join("");
const cases = (word: string) => {
const _c = new Set([word, ucfirst(word), word.toLowerCase(), word.toUpperCase()]);
return `(?:${Array.from(_c).join('|')})`;
};
// acronym("ACS") -> "(?:ACS(?!\.)|A\.C\.S\.)"
// Plain form (not followed by a dot, so "ACS." is rejected) OR dotted "A.C.S.".
// Wrapped in (?:...) so the internal | can't leak into the surrounding
// alternation — this is what makes the "BP/USP" combos parse correctly.
const acronym = (word: string) => `(?:${word}(?!\\.)|${word.replaceAll(/([A-Z])/g, '$1\\.')})`;
// ── Core reusable tokens ────────────────────────────────────────────────────
const gradeTxt = cases('grade');
const purityTxt = cases('purity');
const qualityTxt = cases('quality');
const reagentTxt = cases('reagent');
// Optional trailing " grade" (lets "AR" also match "AR Grade").
const optionalGrade = String.raw`(?:\s+${gradeTxt})?`;
// Flexible "reagent"/"grade" tail in EITHER order.
const rgFlex = String.raw`(?:${reagentTxt}(?:\s+${gradeTxt})?|${gradeTxt}(?:\s+${reagentTxt})?)`;
// Pharma stem. The three endings are siblings, not nested — "cy"/"ceutical" branch off
// "pharma", NOT off "pharmacop":
// pharma | pharmacop | pharmacopeia | pharmacopoeia | pharmacy | pharmaceutical
const pharma = String.raw`${cases('pharma')}(?:${cases('cop')}(?:[Oo]?${cases('eia')})?|${cases('cy')}|${cases('ceutical')})?`;
// ── Word-grade stems ────────────────────────────────────────────────────────
// The bare qualifier word for each grade that has no acronym of its own. On its
// own a stem is too weak to classify ("ultra pure water", "low prices"), so the
// bodies below all pair it with a required reagent/grade tail. The one place a
// bare stem IS decisive is after an explicit "Grade:"/"Purity:" label, which is
// what buildLabeledGradeRegex reuses these for.
const stems = {
Guaranteed_Grade: cases('guaranteed'),
Cosmetic_Grade: cases('cosmetic'),
Extraction_Grade: cases('extraction'),
Practical_Grade: cases('practical'),
// [IiUu] absorbs the "indistrial" typo.
Industrial_Grade: String.raw`${cases('ind')}[IiUu]${cases('strial')}`,
Technical_Grade: String.raw`${cases('tech')}(?:${cases('nical')})?`,
Reagent_Grade: reagentTxt,
// "lab" or laboratory + its two common misspellings.
Lab_Grade: String.raw`${cases('lab')}(?:${cases('oratory')}|${cases('oratiry')}|${cases('pratory')})?`,
Pure_Grade: String.raw`${cases('pur')}(?:${cases('e')}|${cases('ified')})`,
High_Purity_Grade: String.raw`(?:${cases('ultra')}\s+)?${cases('high')}\s+(?:${cases('purity')}|${qualityTxt}|${rgFlex})`,
Pharma_Grade: pharma,
Low_Grade: cases('low'),
};
// ── Per-grade group bodies ──────────────────────────────────────────────────
const bodies = {
AR_Grade: String.raw`(?:${acronym('AR')}|${cases('analytical')}(?:\s*${reagentTxt})?)${optionalGrade}`,
ACS_Grade: String.raw`(?:${acronym('ACS')}|${cases('acs')}\s+${gradeTxt}|${cases('american')}\s+${cases('chem')}(?:${cases('ical')})?\s+${cases('society')})${optionalGrade}`,
Guaranteed_Grade: String.raw`${stems.Guaranteed_Grade}\s+${rgFlex}`,
Cosmetic_Grade: String.raw`${stems.Cosmetic_Grade}\s+${rgFlex}`,
Extraction_Grade: String.raw`${stems.Extraction_Grade}\s+${rgFlex}`,
NF_Grade: String.raw`(?:${acronym('NF')}|${cases('nf')}\s+${gradeTxt}|${cases('national')}\s+${cases('formulary')})${optionalGrade}`,
FCC_Grade: String.raw`(?:${acronym('FCC')}|${acronym('NSF')}|${cases('fcc')}\s+${gradeTxt}|${cases('food')}\s+(?:${cases('chem')}(?:${cases('icals')})?\s+${cases('codex')}|${rgFlex}))${optionalGrade}`,
Practical_Grade: String.raw`${stems.Practical_Grade}\s+${rgFlex}`,
Industrial_Grade: String.raw`${stems.Industrial_Grade}\s+${rgFlex}`,
// "tech"/"technical" + flexible tail (any case), OR bare fully-uppercase
// "TECHNICAL" / "TECHNICAL GRADE" (all-caps product titles).
Technical_Grade: String.raw`(?:${stems.Technical_Grade}\s+${rgFlex}|TECHNICAL(?:\s+GRADE)?)`,
// Bare "reagent grade". Qualifier-prefixed forms ("Practical/Technical/Pure/…
// Reagent Grade") are claimed by those groups, which all consume the reagent
// tail — so ordering keeps this from firing on them, no lookbehind needed.
Reagent_Grade: String.raw`(?<!(CS|SP|CC|AR|BP|JP|PA|AL|al)\s)${stems.Reagent_Grade}${optionalGrade}(?!.*(ACS|(US|J|B)P|NF|FCC|HPLC).*)`,
// "BP"/"B.P." — decline when "/USP" or "/U.S.P." follows so the combo routes
// to USP. Or "britt?ish pharmacop..." (t? absorbs the "Brittish" typo).
BP_Grade: String.raw`(?:${acronym('BP')}(?!\s*/\s*${acronym('USP')})|${cases('brit')}[Tt]?${cases('ish')}\s+${pharma})${optionalGrade}`,
JP_Grade: String.raw`(?:${acronym('JP')}|${cases('japanese')}\s+${pharma})${optionalGrade}`,
// Combined designations FIRST (so the whole "BP/USP" is captured), then
// "USP"/"U.S.P." / "usp grade" / "United States|US pharmacop...".
USP_Grade: String.raw`(?:${acronym('BP')}\s*/\s*${acronym('USP')}|${acronym('USP')}\s*/\s*${acronym('BP')}|${acronym('USP')}|${cases('usp')}\s+${gradeTxt}|(?:${cases('united')}\s+${cases('states')}|${acronym('US')})\s+${pharma})${optionalGrade}`,
HPLC_Grade: String.raw`(?:${acronym('HPLC')}|${cases('hplc')}\s+${gradeTxt}|${cases('gradient')}\s+${gradeTxt}|${cases('high')}[-\s]+${cases('performance')}\s+${cases('liquid')}\s+${cases('chromatography')})${optionalGrade}`,
Lab_Grade: String.raw`(?:${acronym('LR')}|${stems.Lab_Grade}\s+${rgFlex})`,
Pure_Grade: String.raw`(?:${acronym('PA')}|${stems.High_Purity_Grade}|${stems.Pure_Grade}\s+${rgFlex})`,
Pharma_Grade: String.raw`${stems.Pharma_Grade}\s+${rgFlex}`,
Low_Grade: String.raw`${stems.Low_Grade}\s+(?:${rgFlex}|${purityTxt})`,
Impure: String.raw`${cases('impure')}(?:\s+${reagentTxt})?`,
Ungraded: String.raw`${cases('ungraded')}(?:\s+${cases('purity')})?(?:\s+${reagentTxt})?`,
};
// ── Assembly ────────────────────────────────────────────────────────────────
// Order matters only for the BP -> USP fallthrough (BP declines "/USP" via the
// lookahead, so USP wins). Other groups have distinct leading tokens.
const order = [
'AR_Grade',
'ACS_Grade',
'Guaranteed_Grade',
'Cosmetic_Grade',
'Extraction_Grade',
'NF_Grade',
'FCC_Grade',
'Practical_Grade',
'Industrial_Grade',
'Technical_Grade',
'Reagent_Grade',
'BP_Grade',
'JP_Grade',
'USP_Grade',
'HPLC_Grade',
'Lab_Grade',
'Pure_Grade',
'Pharma_Grade',
'Low_Grade',
'Impure',
'Ungraded',
] as const;
const namedGroups = order.map((name) => `(?<${name}>${bodies[name]})`).join('|');
// \b...(?!\w): matches a grade token anywhere. (?!\w) — rather than a trailing
// \b — lets a token end on a dot ("A.R.", "U.S.P."). Swap the \b for ^ and the
// (?!\w) for $ for a strict full-string match.
const classifier = new RegExp(String.raw`\b(?:${namedGroups})(?!\w)`);
// ── Labeled fallback ────────────────────────────────────────────────────────
// Suppliers also write the grade as a labeled field: "Grade: Technical".
// The word grades can't match that off `classifier` alone, because their bodies
// require a reagent/grade tail to keep prose like "ultra pure water" from
// classifying. An explicit "Grade:"/"Purity:"/"Quality:" label supplies that
// missing signal, so here the bare stem is enough. Acronym grades never reach
// this regex — they already match the classifier on their own.
const labelPrefix = String.raw`\b(?:${gradeTxt}|${purityTxt}|${cases('quality')})\s*[:\-–]\s*`;
const labeledGroups = Object.entries(stems)
.map(([name, stem]) => `(?<${name}>${stem})`)
.join('|');
const labeled = new RegExp(String.raw`${labelPrefix}(?:${labeledGroups})(?!\w)`);
const patterns = { classifier, labeled };
return patterns;
};
Build grade regexes