Functional Weave
Code in Rust

hospitality.allergen-matrix@1.0.1

impl/typescript.ts

2,733 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { addDays } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { ALLERGENS, ALLERGENS_HISTORY, type Allergen } from "./hospitality_allergen_matrix_data.ts";  ← this capability’s own data, compiled from data/allergens.json into the same file by fune build
import { type AllergenRow, type IngredientAllergens } from "./hospitality_allergen_matrix_types.ts";

function listInForce(jurisdiction: string, onDate: string): Allergen[] {
  const rows = ALLERGENS.filter(
    (a) => a.jurisdiction === jurisdiction && a.validFrom <= onDate && (a.validTo === null || onDate <= a.validTo),
  );
  if (rows.length > 0) return [...rows].sort((a, b) => a.annexNumber - b.annexNumber);
  // A history-pruned build may have dropped the list that applied then; say so
  // rather than answering with nothing.
  if (ALLERGENS_HISTORY !== "full") {
    const earliest = ALLERGENS.filter((a) => a.jurisdiction === jurisdiction).reduce<string | null>(
      (e, a) => (e === null || a.validFrom < e ? a.validFrom : e),
      null,
    );
    if (earliest !== null && onDate < earliest) {
      throw new RangeError(
        `no allergen list for ${jurisdiction} on ${onDate}: this build was installed with history=${ALLERGENS_HISTORY}, so it only carries lists from ${earliest}`,
      );
    }
  }
  throw new RangeError(`no allergen list for ${jurisdiction} on ${onDate}`);
}

/**
 * The regulated allergens in a dish, from what its ingredients declare.
 *
 * Every allergen on the list gets a row, in the legal order, so a menu or
 * allergen chart shows "none" as a decision rather than a gap. An allergen any
 * ingredient contains is "contains"; otherwise one any ingredient may contain
 * is "may-contain". The sources say which ingredients to check.
 */
export function allergenMatrix(ingredients: readonly IngredientAllergens[], jurisdiction: string, onDate: string): readonly AllergenRow[] {
  addDays(onDate, 0);
  const list = listInForce(jurisdiction, onDate);
  const known = new Set(list.map((a) => a.code));
  for (const ingredient of ingredients) {
    for (const code of [...ingredient.contains, ...ingredient.mayContain]) {
      if (!known.has(code)) {
        throw new RangeError(`unknown allergen "${code}" in "${ingredient.name}": not on the ${jurisdiction} list on ${onDate}`);
      }
    }
  }
  return list.map((allergen) => {
    const contains = ingredients.filter((i) => i.contains.includes(allergen.code)).map((i) => i.name);
    const may = ingredients.filter((i) => i.mayContain.includes(allergen.code)).map((i) => i.name);
    const sources = contains.length > 0 ? contains : may;
    return {
      code: allergen.code,
      name: allergen.name,
      status: contains.length > 0 ? "contains" : may.length > 0 ? "may-contain" : "none",
      sources: sources.filter((name, i) => sources.indexOf(name) === i),
    };
  });
}