ChemPal Documentation - v1.6.0
    Preparing search index...

    Function buildGradeRegexes

    • Build grade regexes

      Returns GradeRegexes

      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;
      };