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