// Calsa food groups for imported records (spec 20, FD-09 "Группы"): the closed list below, assigned // from the source's own category by a reviewed table, never by guessing from the name. // // A category that spans several Calsa groups (USDA "Dairy and Egg Products", "Beverages"; CNF // "Cereals, Grains and Pasta"; AFCD/AUSNUT 5-digit codes that are not listed) maps to null: the // record keeps food_group = null, is quarantined as `unmapped_group` and stays out of search // (FD-09, FD-43) until a table row decides it. Infant food (C-23) and supplements (C-26) are not // part of the base: the tables mark them 'infant' / 'supplement' and the importers skip them. export const FOOD_GROUPS = [ 'fruit', 'vegetables', 'legumes', 'grains_cereals', 'bread_bakery', 'pasta_rice', 'meat', 'poultry', 'fish_seafood', 'eggs', 'dairy', 'plant_alternatives', 'nuts_seeds', 'fats_oils', 'sweets_desserts', 'snacks', 'drinks', 'alcohol', 'sauces_condiments', 'soups', 'mixed_dishes', 'sports_nutrition', ] as const; export type FoodGroup = typeof FOOD_GROUPS[number]; export type Excluded = 'infant' | 'supplement'; // A table answer: a group, a record that is not in the base, or null for "not decided". export type GroupEntry = FoodGroup | Excluded | null; export const UNMAPPED_REASON = 'unmapped_group'; export function isExcluded(entry: GroupEntry): entry is Excluded { return entry === 'infant' || entry === 'supplement'; } export const excludedReason = (entry: Excluded) => entry === 'infant' ? 'baby food (C-23)' : 'dietary supplement (C-26)'; // The group a stored record gets: excluded records never reach the insert, so they count as none. export const storedGroup = (entry: GroupEntry): FoodGroup | null => isExcluded(entry) ? null : entry; type Spec = Partial>; // The tables are written group by group (easier to review) and inverted once; a source category // listed twice is a mistake in the table and stops the module from loading. function table(name: string, spec: Spec): ReadonlyMap { const out = new Map(); for (const [group, keys] of Object.entries(spec)) { for (const key of keys ?? []) { if (out.has(key)) throw new Error(`${name}: ${key} is mapped twice`); out.set(key, group === 'unmapped' ? null : group as GroupEntry); } } return out; } const lookup = (map: ReadonlyMap, key: string | null | undefined): GroupEntry => key === null || key === undefined ? null : map.get(key) ?? null; // ---- USDA FoodData Central ------------------------------------------------------------------- // Foundation and SR Legacy: foodCategory.description (the food-group names of the SR tables). export const usdaFoodCategories = table('usda foodCategory', { fruit: ['Fruits and Fruit Juices'], vegetables: ['Vegetables and Vegetable Products'], legumes: ['Legumes and Legume Products'], grains_cereals: ['Breakfast Cereals'], bread_bakery: ['Baked Products'], meat: ['Beef Products', 'Pork Products', 'Lamb, Veal, and Game Products', 'Sausages and Luncheon Meats'], poultry: ['Poultry Products'], fish_seafood: ['Finfish and Shellfish Products'], nuts_seeds: ['Nut and Seed Products'], fats_oils: ['Fats and Oils'], sweets_desserts: ['Sweets'], snacks: ['Snacks'], sauces_condiments: ['Spices and Herbs'], mixed_dishes: ['Meals, Entrees, and Side Dishes'], infant: ['Baby Foods'], // Each of these holds foods of two or more Calsa groups. unmapped: [ 'Dairy and Egg Products', // dairy / eggs 'Beverages', // drinks / alcohol / sports_nutrition 'Cereal Grains and Pasta', // grains_cereals / pasta_rice 'Soups, Sauces, and Gravies', // soups / sauces_condiments 'Fast Foods', 'Restaurant Foods', // group by composition (FD-09), not by outlet 'American Indian/Alaska Native Foods', // any group ], }); // FNDDS (Survey): wweiaFoodCategory.wweiaFoodCategoryDescription. export const usdaWweiaCategories = table('usda wweia', { fruit: ['Citrus fruits', 'Dried fruits', 'Apples', 'Bananas', 'Blueberries and other berries', 'Grapes', 'Mango and papaya', 'Melons', 'Other fruits and fruit salads', 'Peaches and nectarines', 'Pears', 'Pineapple', 'Strawberries'], vegetables: ['Broccoli', 'Cabbage', 'Carrots', 'Coleslaw, non-lettuce salads', 'Corn', 'French fries and other fried white potatoes', 'Fried vegetables', 'Lettuce and lettuce salads', 'Mashed potatoes and white potato mixtures', 'Onions', 'Other dark green vegetables', 'Other red and orange vegetables', 'Other starchy vegetables', 'Other vegetables and combinations', 'Spinach', 'String beans', 'Tomatoes', 'Vegetables on a sandwich', 'White potatoes, baked or boiled'], legumes: ['Beans, peas, legumes'], grains_cereals: ['Grits and other cooked cereals', 'Oatmeal', 'Ready-to-eat cereal, higher sugar (>21.2g/100g)', 'Ready-to-eat cereal, lower sugar (=<21.2g/100g)'], bread_bakery: ['Bagels and English muffins', 'Biscuits, muffins, quick breads', 'Pancakes, waffles, French toast', 'Rolls and buns', 'Tortillas', 'Yeast breads'], pasta_rice: ['Pasta, noodles, cooked grains', 'Rice'], meat: ['Bacon', 'Beef, excludes ground', 'Cold cuts and cured meats', 'Frankfurters', 'Ground beef', 'Lamb, goat, game', 'Liver and organ meats', 'Pork', 'Sausages'], poultry: ['Chicken patties, nuggets and tenders', 'Chicken, whole pieces', 'Turkey, duck, other poultry'], fish_seafood: ['Fish', 'Shellfish'], eggs: ['Eggs and omelets'], dairy: ['Cheese', 'Cottage/ricotta cheese', 'Cream and cream substitutes', 'Cream cheese, sour cream, whipped cream', 'Flavored milk, lowfat', 'Flavored milk, nonfat', 'Flavored milk, reduced fat', 'Flavored milk, whole', 'Milk shakes and other dairy drinks', 'Milk, lowfat', 'Milk, nonfat', 'Milk, reduced fat', 'Milk, whole', 'Yogurt, Greek', 'Yogurt, regular'], plant_alternatives: ['Plant-based milk', 'Plant-based yogurt', 'Soy and meat-alternative products'], nuts_seeds: ['Nuts and seeds'], fats_oils: ['Butter and animal fats', 'Margarine'], sweets_desserts: ['Cakes and pies', 'Candy containing chocolate', 'Candy not containing chocolate', 'Cookies and brownies', 'Doughnuts, sweet rolls, pastries', 'Gelatins, ices, sorbets', 'Ice cream and frozen dairy desserts', 'Jams, syrups, toppings', 'Pudding', 'Sugars and honey'], snacks: ['Cereal bars', 'Crackers, excludes saltines', 'Popcorn', 'Potato chips', 'Pretzels/snack mix', 'Saltine crackers', 'Tortilla, corn, other chips'], drinks: ['Apple juice', 'Bottled water', 'Citrus juice', 'Coffee', 'Diet soft drinks', 'Diet sport and energy drinks', 'Enhanced water', 'Flavored or carbonated water', 'Fruit drinks', 'Other diet drinks', 'Other fruit juice', 'Smoothies and grain drinks', 'Soft drinks', 'Sport and energy drinks', 'Tap water', 'Tea', 'Vegetable juice'], alcohol: ['Beer', 'Liquor and cocktails', 'Wine'], sauces_condiments: ['Dips, gravies, other sauces', 'Mayonnaise', 'Mustard and other condiments', 'Pasta sauces, tomato-based', 'Soy-based condiments', 'Tomato-based condiments'], soups: ['Ramen and Asian broth-based soups', 'Soups, broth-based', 'Soups, cream-based'], mixed_dishes: ['Bean, pea, legume dishes', 'Burgers', 'Burritos and tacos', 'Cheese sandwiches', 'Chicken fillet sandwiches', 'Deli and cured meat sandwiches', 'Egg rolls, dumplings, sushi', 'Egg/breakfast sandwiches', 'Fried rice and lo/chow mein', 'Frankfurter sandwiches', 'Macaroni and cheese', 'Meat and BBQ sandwiches', 'Meat mixed dishes', 'Nachos', 'Other Mexican mixed dishes', 'Pasta mixed dishes, excludes macaroni and cheese', 'Peanut butter and jelly sandwiches', 'Pizza', 'Poultry mixed dishes', 'Rice mixed dishes', 'Seafood mixed dishes', 'Seafood sandwiches', 'Stir-fry and soy-based sauce mixtures', 'Turnovers and other grain-based items', 'Vegetable dishes', 'Vegetable sandwiches/burgers'], sports_nutrition: ['Nutrition bars', 'Nutritional beverages', 'Protein and nutritional powders'], infant: ['Baby food: cereals', 'Baby food: fruit', 'Baby food: meat and dinners', 'Baby food: mixtures', 'Baby food: snacks and sweets', 'Baby food: vegetables', 'Baby food: yogurt', 'Baby juice', 'Baby water', 'Formula, prepared from powder', 'Formula, ready-to-feed', 'Human milk'], unmapped: [ 'Not included in a food category', 'Salad dressings and vegetable oils', // sauces_condiments / fats_oils 'Olives, pickles, pickled vegetables', // olives are a high-fat fruit, the rest vegetables 'Sugar substitutes', // not sugar, not a food group of its own ], }); export type UsdaDataset = 'foundation' | 'sr_legacy' | 'survey'; // foodCategory is the grouping of Foundation and SR Legacy, WWEIA the one of FNDDS. export function usdaGroup(dataset: UsdaDataset, food: { foodCategory?: { description?: string }; wweiaFoodCategory?: { wweiaFoodCategoryDescription?: string }; }): GroupEntry { return dataset === 'survey' ? lookup(usdaWweiaCategories, food.wweiaFoodCategory?.wweiaFoodCategoryDescription) : lookup(usdaFoodCategories, food.foodCategory?.description); } // Branded (FDC brandedFoodCategory, the category printed in the label data). Free text from three // vocabularies (US, Australian, mixed); the empty category is its own key. export const usdaBrandedCategories = table('usda branded', { fruit: ['Canned Fruit', 'Fruit - Prepared/Processed', 'Fruit - Prepared/Processed (Shelf Stable)', 'Fruit Prepared/Processed', 'Frozen Fruit', 'Fruits - Unprepared/Unprocessed (Frozen)', 'Fruits - Unprepared/Unprocessed (Shelf Stable)', 'Fruit', 'Berries/Small Fruit', 'Snack Foods - Dried Fruit'], vegetables: ['Canned Vegetables', 'Frozen Vegetables', 'Tomatoes', 'Vegetables - Prepared/Processed', 'Vegetables Prepared/Processed', 'French Fries, Potatoes & Onion Rings', 'Canned/Dried Veges', 'Vegetables - Prepared/Processed (Shelf Stable)', 'Vegetables - Unprepared/Unprocessed (Shelf Stable)', 'Vegetables - Unprepared/Unprocessed (Frozen)', 'Vegetables Unprepared/Unprocessed (Frozen)', 'Vegetables', 'Frozen Potato', 'Peppers'], legumes: ['Canned & Bottled Beans', 'Chickpeas'], grains_cereals: ['Cereal', 'Processed Cereal Products', 'Flours & Corn Meal', 'Grains/Flour', 'Cereals Products - Ready to Eat (Shelf Stable)', 'Breakfast Cereals - Hot And Cold', 'Cereals Products - Not Ready to Eat (Shelf Stable)', 'Flour - Cereal/Pulse (Shelf Stable)'], bread_bakery: ['Breads & Buns', 'Croissants, Sweet Rolls, Muffins & Other Pastries', 'Crusts & Dough', 'Frozen Bread & Dough', 'Bread & Muffin Mixes', 'Frozen Pancakes, Waffles, French Toast & Crepes', 'Savoury Bakery Products', 'Bread', 'Pastry Shells & Fillings', 'Pancakes, Waffles, French Toast & Crepes', 'Bread/Bakery Products Variety Packs', 'Bread - Incl. Buns And Rolls', 'Taco Shells', 'Pastry'], pasta_rice: ['Pasta by Shape & Type', 'Rice', 'All Noodles', 'Pasta/Noodles', 'Pasta', 'Noodles', 'Fresh Pasta'], meat: ['Pepperoni, Salami & Cold Cuts', 'Sausages, Hotdogs & Brats', 'Other Meats', 'Bacon, Sausages & Ribs', 'Canned Meat', 'Frozen Bacon, Sausages & Ribs', 'Frozen Sausages, Hotdogs & Brats', 'Meat/Poultry/Other Animals Sausages - Prepared/Processed', 'Meat/Poultry/Other Animals Sausages Prepared/Processed', 'Sausages/Smallgoods', 'Bacon', 'Frozen Meat', 'Salami / Cured Meat', 'Ham/Cold Meats', 'Pork Sausages - Prepared/Processed', 'Pork - Unprepared/Unprocessed', 'Pork - Prepared/Processed', 'Beef - Prepared/Processed'], poultry: ['Poultry, Chicken & Turkey', 'Frozen Poultry, Chicken & Turkey', 'Frozen Chicken - Processed', 'Fresh Chicken - Processed', 'Frozen Chicken - Portions', 'Turkey - Unprepared/Unprocessed', 'Fresh Chicken - Whole', 'Fresh Chicken - Portions'], fish_seafood: ['Frozen Fish & Seafood', 'Canned Seafood', 'Fish & Seafood', 'Canned Tuna', 'Fish Unprepared/Unprocessed', 'Shellfish Unprepared/Unprocessed', 'Frozen Fish/Seafood', 'Fish - Prepared/Processed', 'Seafood Miscellaneous', 'Smoked fish', 'Shellfish Prepared/Processed', 'Fish Prepared/Processed', 'Aquatic Invertebrates/Fish/Shellfish/Seafood Combination', 'S/O Fresh Fish Fillets/Steaks', 'Mussels'], eggs: ['Eggs & Egg Substitutes', 'Eggs/Eggs Substitutes', 'Eggs'], dairy: ['Cheese', 'Yogurt', 'Milk', 'Cream', 'Cheese - Speciality ', 'Dairy Foods/Yoghurts', 'Milk/Cream - Shelf Stable', 'Cheese - Block', 'Cream/Cream Substitutes', 'Milk/Cream', 'Processed Cheese & Cheese Novelties'], plant_alternatives: ['Plant Based Milk', 'Vegetarian Frozen Meats', 'Fish Substitutes', 'Tofu'], nuts_seeds: ['Nut & Seed Butters', 'Nuts/Seeds - Prepared/Processed', 'Nuts/Seeds Prepared/Processed', 'Nuts/Seeds - Prepared/Processed (Shelf Stable)', 'Snack Foods - Nuts', 'Nuts/Seeds - Unprepared/Unprocessed (In Shell)'], fats_oils: ['Vegetable & Cooking Oils', 'Butter & Spread', 'Oils Edible', 'Butter/Butter Substitutes', 'Fats Edible', 'Cooking Oils and Fats', 'Margarine/Butter'], sweets_desserts: ['Candy', 'Ice Cream & Frozen Yogurt', 'Cookies & Biscuits', 'Chocolate', 'Cakes, Cupcakes, Snack Cakes', 'Baking Decorations & Dessert Toppings', 'Cake, Cookie & Cupcake Mixes', 'Jam, Jelly & Fruit Spreads', 'Other Frozen Desserts', 'Syrups & Molasses', 'Biscuits/Cookies', 'Granulated, Brown & Powdered Sugar', 'Chewing Gum & Mints', 'Honey', 'Puddings & Custards', 'Confectionery Products', 'Gelatin, Gels, Pectins & Desserts', 'Sweet Bakery Products', 'Desserts/Dessert Sauces/Toppings', 'Biscuits/Cookies (Shelf Stable)', 'Confectionery', 'Sweet Spreads', 'Cakes - Sweet (Frozen)', 'Pies/Pastries - Sweet (Shelf Stable)', 'Ice Cream/Ice Novelties (Shelf Stable)', 'Ice-Cream Take Home', 'Biscuits Chocolate', 'Cakes and Slices', 'Desserts & Custard', 'Puddings and desserts', 'Biscuits Plain/Sweet', 'Ice-Cream/Ice-Block Multi Pack', 'Frozen Desserts', 'Ice-Cream/Block Single', 'Cakes/Slices/Biscuits', 'Cakes - Sweet (Shelf Stable)', 'Biscuits Kids'], snacks: ['Popcorn, Peanuts, Seeds & Related Snacks', 'Chips, Pretzels & Snacks', 'Snack, Energy & Granola Bars', 'Other Snacks', 'Wholesome Snacks', 'Crackers & Biscotti', 'Snacks', 'Lunch Snacks & Combinations', 'Flavored Snack Crackers', 'Cereal/Muesli Bars', 'Chips/Crisps/Snack Mixes - Natural/Extruded (Shelf Stable)', 'Snack Foods - Other', 'Snack Foods - Chips', 'Biscuits Cracker', 'Wrapped Snacks - Muesli Bars', 'Snack Foods - Cereal Snacks', 'Wrapped Snacks - Nut Bars', 'Popcorn (Shelf Stable)', 'Wrapped Snacks - Cereal', 'Snack Foods - Corn Chips', 'Frozen Snack', 'Wrapped Snacks - Fruit Bars', 'Snack Foods - Multi Packs'], drinks: ['Fruit & Vegetable Juice, Nectars & Fruit Drinks', 'Soda', 'Water', 'Other Drinks', 'Powdered Drinks', 'Iced & Bottle Tea', 'Tea Bags', 'Non Alcoholic Beverages - Ready to Drink', 'Sport Drinks', 'Liquid Water Enhancer', 'Plant Based Water', 'Non Alcoholic Beverages Ready to Drink', 'Coffee', 'Non Alcoholic Beverages Not Ready to Drink', 'Non Alcoholic Beverages - Not Ready to Drink', 'Breakfast Drinks', 'Coffee/Tea/Substitutes', 'Tea and Infusions/Tisanes', 'Drinks Flavoured - Ready to Drink', 'Coffee/Coffee Substitutes', 'Drinks - Juices, Drinks and Cordials', 'Drinks - Soft Drinks', 'Drinks - Powdered', 'Ready To Drink', 'Tea - Bags, Loose Leaf, Speciality', 'Drinks - Energy Drinks', 'Coffee - Instant, Roast and Ground'], alcohol: ['Alcohol', 'Alcoholic Beverages', 'Beer'], sauces_condiments: ['Pickles, Olives, Peppers & Relishes', 'Seasoning Mixes, Salts, Marinades & Tenderizers', 'Dips & Salsa', 'Ketchup, Mustard, BBQ & Cheese Sauce', 'Salad Dressing & Mayonnaise', 'Prepared Pasta & Pizza Sauces', 'Oriental, Mexican & Ethnic Sauces', 'Herbs & Spices', 'Other Cooking Sauces', 'Sauces/Spreads/Dips/Condiments', 'Gravy Mix', 'Other Condiments', 'Herbs/Spices/Extracts', 'Pickles/Relishes/Chutneys/Olives', 'Sauces', 'Seasonings/Preservatives/Extracts Variety Packs', 'Dips/Hummus/Pate', 'Salad Dressings', 'Herbs And Spices', 'Sauces- Cooking', 'Pickles, Relishes and Vinegar', 'Vinegars/Cooking Wines', 'Dressings/Dips (Shelf Stable)', 'Dips - Shelf Stable', 'Sauces - Cooking (Shelf Stable)'], soups: ['Other Soups', 'Canned Soup', 'Prepared Soups', 'Canned Condensed Soup', 'Soups - Prepared (Shelf Stable)'], mixed_dishes: ['Frozen Dinners & Entrees', 'Frozen Appetizers & Hors D\'oeuvres', 'Pizza', 'Mexican Dinner Mixes', 'Pasta Dinners', 'Entrees, Sides & Small Meals', 'Deli Salads', 'Cooked & Prepared', 'Chili & Stew', 'Prepared Subs & Sandwiches', 'Frozen Breakfast Sandwiches, Biscuits & Meals', 'Frozen Prepared Sides', 'Flavored Rice Dishes', 'Prepared Wraps and Burittos', 'Pizza Mixes & Other Dry Dinners', 'Sushi', 'Dough Based Products / Meals', 'Vegetable Based Products / Meals', 'Sandwiches/Filled Rolls/Wraps', 'Stuffing', 'Breakfast Sandwiches, Biscuits & Meals', 'Ready-Made Combination Meals', 'Grain Based Products / Meals', 'Prepared Meals', 'Vegetable Based Products / Meals - Not Ready to Eat (Frozen)', 'Frozen Meals', 'Pies/Pastries/Pizzas/Quiches - Savoury (Frozen)', 'Frozen Pies/Savouries', 'Dough Based Products / Meals - Not Ready to Eat - Savoury (Shelf Stable)', 'Egg Based Products / Meals - Not Ready to Eat (Frozen)', 'Pasta - Instant Meals', 'Potato - Instant Meals', 'Grain Based Products / Meals - Not Ready to Eat - Savoury (Shelf Stable)', 'Dough Based Products / Meals - Not Ready to Eat - Savoury (Frozen)'], sports_nutrition: ['Energy, Protein & Muscle Recovery Drinks', 'Meal Replacement Supplements', 'Sports and Weight Management'], infant: ['Baby/Infant Foods/Beverages', 'Baby/Infant - Foods/Beverages', 'Infant Formula'], supplement: ['Specialty Formula Supplements', 'Green Supplements', 'Herbal Supplements', 'Digestive & Fiber Supplements', 'Children\'s Nutritional Supplements', 'Vitamins', 'Health Supplements and Vitamins', 'Fatty Acid Supplements', 'Vitamins/Minerals/Nutritional Supplements', 'Minerals', 'Children\'s Natural Remedies', 'Antioxidant Supplements', 'Amino Acid Supplements'], unmapped: ['', 'Pre-Packaged Fruit & Vegetables', 'Other Deli', 'Milk/Milk Substitutes', 'Frozen Fruit & Fruit Juice Concentrates', 'Vegetable and Lentil Mixes', 'Milk Additives', 'Meat/Poultry/Other Animals - Prepared/Processed', 'Frozen Patties and Burgers', 'Other Grains & Seeds', 'Baking/Cooking Mixes/Supplies', 'Baking Additives & Extracts', 'Meat/Poultry/Other Animals Unprepared/Unprocessed', 'Yogurt/Yogurt Substitutes', 'Cheese/Cheese Substitutes', 'Other Frozen Meats', 'Meat/Poultry/Other Animals Prepared/Processed', 'Meat/Poultry/Other Animals - Unprepared/Unprocessed', 'Prepared/Preserved Foods Variety Packs', 'Weight Control', 'Spreads', 'Canned Fish and Meat', 'Health Care', 'Fruit/Nuts/Seeds Combination', 'Baking Needs', 'Sugars/Sugar Substitute Products', 'Dairy/Egg Based Products / Meals', 'Sugar And Flour', 'Baking/Cooking Supplies (Shelf Stable)', 'Vegetarian', 'Food/Beverage/Tobacco Variety Packs', 'Salads', 'Frozen Foods', 'Pastries/Pies/Pizzas', 'Fresh Meat', 'Baking', 'Baking/Cooking Mixes (Shelf Stable)', 'Yogurt/Yogurt Substitutes (Perishable)', 'Rice & Grains', 'Baking/Cooking Mixes (Perishable)', 'Baking Accessories', 'Oral Hygiene Products', 'Miscellanious', 'Fruits/Vegetables/Nuts/Seeds Variety Packs', 'Fruits, Vegetables & Produce', 'Deli and Chilled Foods', 'Antipasto', 'Media', 'Gardening', 'Fresh Meat, Poultry and Seafood', 'Fresh Fruit and Vegetables', 'Breakfast Foods', 'Baking/Cooking Mixes/Supplies Variety Packs'], }); // For the Branded importer: pass FDC `brandedFoodCategory` (a missing one is the empty string). export const brandedGroup = (category: string | null | undefined): GroupEntry => lookup(usdaBrandedCategories, category ?? ''); // ---- CoFID (UK) ------------------------------------------------------------------------------ // The "Group" column of the factors sheet: a letter code with up to three letters. Reviewed on // the foods of every code in the 2021 integrated dataset. export const cofidGroups = table('cofid', { bread_bakery: ['AF', 'AG', 'AK', 'AN', 'AO', 'AP', 'AS', 'AT'], grains_cereals: ['A', 'AA', 'AI'], pasta_rice: ['AC', 'AD'], mixed_dishes: ['AB', 'AE', 'BV', 'CD', 'CDE', 'DR', 'JR', 'MBG', 'MIG', 'MR'], sweets_desserts: ['AM', 'BP', 'BR', 'CDH', 'S', 'SC', 'SEA', 'SEC'], dairy: ['BA', 'BAB', 'BAE', 'BAH', 'BAK', 'BAR', 'BJC', 'BJP', 'BJS', 'BL', 'BN', 'BNE', 'BNH', 'BNS'], fats_oils: ['BTM', 'OA', 'OB', 'OC', 'OE', 'OF'], eggs: ['CA'], vegetables: ['D', 'DA', 'DAE', 'DAM', 'DAP', 'DAR', 'DF', 'DG', 'DI'], legumes: ['DB'], fruit: ['F', 'FA', 'FC'], nuts_seeds: ['G', 'GA'], sauces_condiments: ['H', 'WC', 'WCD', 'WCG', 'WCN', 'WE'], fish_seafood: ['J', 'JA', 'JC', 'JK', 'JM'], meat: ['MAA', 'MAC', 'MAE', 'MAG', 'MAI', 'MEA', 'MEC', 'MEE', 'MG', 'MI'], poultry: ['MCA', 'MCC', 'MCE', 'MCG', 'MCI', 'MCK', 'MCM', 'MCO'], drinks: ['P', 'PA', 'PAA', 'PAC', 'PCA', 'PCC', 'PE'], alcohol: ['Q', 'QA', 'QC', 'QE', 'QF', 'QG', 'QI', 'QK'], snacks: ['SN', 'SNA', 'SNC'], soups: ['WA', 'WAA', 'WAC', 'WAE'], unmapped: [ 'BC', // goat, human and soya milks together 'BH', // cocoa, malted and milk-shake powders 'WY', // baking powder, salt, yeast ], }); export const cofidGroup = (code: string | null | undefined): GroupEntry => lookup(cofidGroups, code?.trim()); // ---- CNF (Canada) ---------------------------------------------------------------------------- // CNF_Food_Group_Code (names from cnf_food_group.csv in the comments). export const cnfGroups = table('cnf', { sauces_condiments: ['2'], // Spices and Herbs fats_oils: ['4'], poultry: ['5'], meat: ['7', '10', '13', '17'], // Sausages and Luncheon meats, Pork, Beef, Lamb, Veal and Game grains_cereals: ['8'], // Breakfast cereals fruit: ['9'], vegetables: ['11'], nuts_seeds: ['12'], fish_seafood: ['15'], legumes: ['16'], bread_bakery: ['18'], // Baked Products sweets_desserts: ['19'], mixed_dishes: ['22'], snacks: ['25'], infant: ['3'], // Baby Foods unmapped: [ '1', // Dairy and Egg Products '6', // Soups, Sauces and Gravies '14', // Beverages '20', // Cereals, Grains and Pasta '21', // Fast Foods ], }); export const cnfGroup = (code: string | null | undefined): GroupEntry => lookup(cnfGroups, code); // ---- AFCD and AUSNUT 2023 (Australia and New Zealand) ---------------------------------------- // Both use the AUSNUT 2023 classification: the 5-digit code of a food starts with its 3-digit // group. The 3-digit table decides unless a 5-digit row says otherwise. export const fsanzGroups3 = table('fsanz 3-digit', { drinks: ['111', '112', '113', '114', '115', '116', '117', '118', '119', '296'], alcohol: ['291', '292', '293', '294', '295'], grains_cereals: ['125', '126'], bread_bakery: ['122', '123'], pasta_rice: ['124'], sweets_desserts: ['131', '133', '195', '196', '197', '271', '272', '273', '281', '282', '284'], snacks: ['132', '261', '262', '263', '264', '283'], mixed_dishes: ['135', '156', '169', '172', '187', '188', '189', '207', '249'], fats_oils: ['141', '142', '143', '144', '145', '146'], fish_seafood: ['151', '152', '153', '154', '155', '157'], fruit: ['160', '161', '162', '163', '164', '165', '166', '167', '168'], eggs: ['171', '174'], meat: ['181', '182', '184', '185', '186'], poultry: ['183'], dairy: ['191', '192', '193', '194', '198'], plant_alternatives: ['201', '202', '203', '204', '205', '206'], soups: ['211', '212', '213', '214', '215', '216'], nuts_seeds: ['221', '222', '223'], sauces_condiments: ['231', '232', '233', '235', '313'], vegetables: ['240', '241', '242', '243', '244', '245', '246', '247', '248'], legumes: ['251'], infant: ['321', '322', '323'], supplement: ['341', '342', '343', '344', '345', '346'], unmapped: [ '121', // grains, rice and flours: decided by the 5-digit code '134', // pastries: decided by the 5-digit code '136', // batter-based products: decided by the 5-digit code '180', // wild harvested meat: decided by the 5-digit code '234', // stuffings '252', // legume products and dishes: decided by the 5-digit code '301', // formulated dietary foods: decided by the 5-digit code '311', '312', '314', '315', // yeast and extracts, sweeteners, essences, raising agents '331', '332', '333', // reptiles, insects, amphibia ], }); // The 5-digit codes whose 3-digit group above is "decided by the 5-digit code" or is overridden. // 13307 (cake mixes), 13602 (batters), 30106 and 30107 (medical foods), 31101 (yeast), 31501 to // 31503 (raising agents, gelatine, additives) are not ready-to-eat foods of a Calsa group. export const fsanzGroups5 = table('fsanz 5-digit', { grains_cereals: ['12101', '12103', '12104'], pasta_rice: ['12102'], bread_bakery: ['13401', '13601', '13604'], sweets_desserts: ['13402', '13403', '13603'], mixed_dishes: ['13404', '13405', '13406', '18002', '24002', '24103', '24903', '24906', '25202', '25203'], meat: ['18001'], vegetables: ['24901', '24902', '24904', '24905'], legumes: ['25201'], sports_nutrition: ['30101', '30102', '30103', '30104', '30105'], sauces_condiments: ['31102', '31103', '31301', '31302', '31303'], soups: ['31304'], unmapped: ['13307', '13602', '30106', '30107', '31101', '31501', '31502', '31503'], }); export function fsanzGroup(code: string | null | undefined): GroupEntry { const key = code?.trim(); if (!key) return null; if (fsanzGroups5.has(key)) return fsanzGroups5.get(key)!; return lookup(fsanzGroups3, key.slice(0, 3)); } // ---- Open Food Facts ------------------------------------------------------------------------- // OFF lists every ancestor of a product's category, so "en:yogurts" also carries "en:dairies" and // "en:fermented-foods". The first tag of this list that the product has decides, so the list runs // from the most specific category to the broadest, and the order is part of the table: a tag // that sits above its parents must stay above them. A product with none of these tags, or whose // first tag is listed as unmapped, has no group. const offOrder: readonly (readonly [GroupEntry, readonly string[]])[] = [ // Not in the base (C-23, C-26) and the sports foods that would otherwise fall into supplements. ['infant', ['en:baby-foods', 'en:infant-formulas', 'en:baby-milks']], ['sports_nutrition', ['en:protein-powders', 'en:protein-bars', 'en:meal-replacements']], ['supplement', ['en:dietary-supplements', 'en:bodybuilding-supplements']], ['alcohol', ['en:alcoholic-beverages']], // Substitutes before the animal foods they imitate. ['plant_alternatives', ['en:plant-based-milk-alternatives', 'en:milk-substitutes', 'en:dairy-substitutes', 'en:meat-alternatives', 'en:meat-analogues', 'en:plant-based-creams']], // Soups are a kind of meal in OFF. ['soups', ['en:soups', 'en:broths']], ['mixed_dishes', ['en:meals', 'en:pizzas-pies-and-quiches', 'en:sandwiches', 'en:baked-beans-in-tomato-sauce']], // Dairy: the product types before the family tags that also hold desserts and drinks. ['dairy', ['en:yogurts', 'en:cheeses', 'en:milks', 'en:creams', 'en:dairy-drinks', 'en:milks-liquid-and-powder']], ['fats_oils', ['en:butters', 'en:margarines', 'en:vegetable-oils', 'en:olive-oils', 'en:animal-fats', 'en:vegetable-fats', 'en:dairy-spreads', 'en:spreadable-fats', 'en:milkfat', 'en:fats']], // Nut butters before legumes: a peanut is a legume to OFF, a nut to Calsa. ['nuts_seeds', ['en:nut-butters', 'en:peanut-butters', 'en:oilseed-purees', 'en:nuts-and-their-products', 'en:nuts', 'en:peanuts']], ['bread_bakery', ['en:breads', 'en:flatbreads', 'en:special-breads', 'en:viennoiseries', 'en:pancakes', 'en:crepes-and-pancakes', 'en:tortillas']], ['sweets_desserts', ['en:ice-creams-and-sorbets', 'en:frozen-desserts', 'en:dairy-desserts', 'en:chocolates', 'en:candies', 'en:confectioneries', 'en:jams', 'en:sweet-spreads', 'en:honeys', 'en:syrups', 'en:sugars', 'en:biscuits', 'en:cakes', 'en:pastries', 'en:sweet-pastries-and-pies', 'en:sweet-pies']], ['snacks', ['en:crackers', 'en:crackers-appetizers', 'en:crisps', 'en:potato-crisps', 'en:popcorn', 'en:corn-chips', 'en:salty-snacks', 'en:salted-snacks', 'en:puffed-cereal-cakes', 'en:cereal-bars', 'en:bars']], ['sweets_desserts', ['en:biscuits-and-cakes', 'en:sweet-snacks', 'en:desserts']], ['snacks', ['en:snacks']], // Rice and pasta before the cereal families that also hold them. ['pasta_rice', ['en:pastas', 'en:rices', 'en:noodles', 'en:rice-noodles']], ['grains_cereals', ['en:breakfast-cereals', 'en:cereal-flakes', 'en:rolled-oats', 'en:flours', 'en:wheat-semolinas', 'en:cereal-semolinas', 'en:groats', 'en:cereal-grains']], ['eggs', ['en:eggs']], ['poultry', ['en:poultries', 'en:chicken-and-its-products']], ['fish_seafood', ['en:seafood', 'en:fishes-and-their-products', 'en:fishes']], ['meat', ['en:meats-and-their-products', 'en:meats', 'en:prepared-meats', 'en:sausages', 'en:hams', 'en:pork-and-its-products', 'en:beef-and-its-products']], // Sauces and pickles before the vegetable and tomato families they also belong to. ['sauces_condiments', ['en:sauces', 'en:condiments', 'en:dips', 'en:salad-dressings', 'en:pickles', 'en:spices', 'en:chutneys', 'en:salsa', 'en:tapenades']], ['legumes', ['en:legumes-and-their-products', 'en:legumes', 'en:canned-legumes', 'en:pulses']], ['nuts_seeds', ['en:seeds']], ['drinks', ['en:fruit-juices', 'en:juices-and-nectars', 'en:waters', 'en:sodas', 'en:carbonated-drinks', 'en:teas', 'en:coffees', 'en:non-alcoholic-beverages', 'en:plant-based-beverages', 'en:beverages', 'en:beverages-and-beverages-preparations']], ['fruit', ['en:fruits-based-foods', 'en:fruit-based-foods', 'en:fruits', 'en:dried-fruits', 'en:canned-fruits']], ['vegetables', ['en:vegetables-based-foods', 'en:vegetables', 'en:canned-vegetables', 'en:frozen-vegetables', 'en:prepared-vegetables', 'en:potatoes-and-their-products', 'en:potatoes', 'en:frozen-fried-potatoes', 'en:fries', 'en:frozen-fries', 'en:mashed-potatoes', 'en:hash-browns', 'en:tomatoes-and-their-products']], ['dairy', ['en:dairies', 'en:fermented-milk-products']], ]; export const offCategoryOrder: readonly (readonly [string, GroupEntry])[] = (() => { const seen = new Set(); return offOrder.flatMap(([entry, tags]) => tags.map(tag => { if (seen.has(tag)) throw new Error(`off categories: ${tag} is listed twice`); seen.add(tag); return [tag, entry] as const; })); })(); // `categories_tags` of an OFF record (all ancestors of its categories). export function offGroup(tags: readonly string[]): GroupEntry { const present = new Set(tags); for (const [tag, entry] of offCategoryOrder) if (present.has(tag)) return entry; return null; } // ---- Ciqual (France) ------------------------------------------------------------------------- // The sub-sub-group (6 digits) decides when it has a row, then the sub-group (4 digits), then the // group (2 digits). Names are the English ones of alim_grp_*.xml. A sub-group that holds foods of // several Calsa groups and has no sub-sub-groups to separate them stays unmapped. export const ciqualGroups = table('ciqual group', { sweets_desserts: ['08'], // ice cream and sorbet fats_oils: ['09'], infant: ['11'], // baby food (C-23) }); export const ciqualSubgroups = table('ciqual sub-group', { mixed_dishes: ['0101', '0103', '0104', '0105', '0106'], // salads, dishes, pizzas and pies, sandwiches, savoury pastries soups: ['0102'], vegetables: ['0201', '0202', '1007'], // vegetables, potatoes and tubers, seaweed legumes: ['0203'], fruit: ['0204'], nuts_seeds: ['0205'], pasta_rice: ['0301'], bread_bakery: ['0302', '0305', '0705'], // breads, pastry dough, Viennese pastries snacks: ['0303', '0708'], // savoury biscuits, cereal bars grains_cereals: ['0304', '0707'], // flours, breakfast cereals meat: ['0401', '0402', '0403', '0404'], fish_seafood: ['0405', '0406', '0407', '0408', '0409'], eggs: ['0410'], plant_alternatives: ['0411', '1009'], dairy: ['0501', '0503', '0504'], drinks: ['0601'], alcohol: ['0603'], sweets_desserts: ['0701', '0702', '0703', '0704', '0706', '0709'], sauces_condiments: ['1001', '1002', '1003', '1004', '1005', '1006'], unmapped: [ '0502', // yoghurts and cheese-like dairy (dairy) beside desserts (sweets_desserts): by sub-sub-group '0602', // juices and soft drinks (drinks) beside plant-based drinks (plant_alternatives): by sub-sub-group '1008', // foods for particular nutritional uses: sports, infant and clinical foods mixed '1010', // plant-based spreads: nut butter, margarine-like and spreads mixed ], }); export const ciqualSubsubgroups = table('ciqual sub-sub-group', { poultry: ['040103', '040104', '040203', '040204'], // poultry and turkey, cooked and raw plant_alternatives: ['040309', '050205', '050306', '060205'], // delicatessen, desserts, cheese, drinks dairy: ['050201', '050202'], // yoghurts, fromages blancs sweets_desserts: ['050203', '050204', '100103'], // dairy and other desserts, dessert sauces drinks: ['060201', '060202', '060203', '060204', '060206', '060207'], }); export function ciqualGroup(group: string | null | undefined, subgroup: string | null | undefined, subsubgroup: string | null | undefined): GroupEntry { for (const [map, key] of [[ciqualSubsubgroups, subsubgroup], [ciqualSubgroups, subgroup], [ciqualGroups, group]] as const) if (key && map.has(key)) return map.get(key)!; return null; } // ---- BLS 4.0 (Germany) ----------------------------------------------------------------------- // The first two characters of the BLS code (letter = main group, digit = sub-group). A prefix that // holds foods of several Calsa groups, or ingredients that are not foods, is left unmapped. export const blsGroups = table('bls', { bread_bakery: ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'D0', 'D9'], grains_cereals: ['C1', 'C2', 'C3', 'C4', 'C5'], plant_alternatives: ['C6', 'H9', 'K8'], sweets_desserts: ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'R3', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7'], eggs: ['E1'], pasta_rice: ['E4', 'E5', 'E6', 'E8', 'E9', 'H0', 'X7', 'X8'], fruit: ['F0', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F8', 'F9'], vegetables: ['G0', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8', 'G9', 'H5', 'H6', 'K1', 'K2', 'K3', 'K4', 'K5', 'K7', 'X5', 'X6'], nuts_seeds: ['H1', 'H2', 'H3', 'H4'], legumes: ['H7'], dairy: ['M0', 'M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8'], drinks: ['N0', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7'], alcohol: ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9'], fats_oils: ['Q1', 'Q2', 'Q3', 'Q4', 'Q6', 'Q7', 'Q8'], sauces_condiments: ['R1', 'R2', 'R8', 'X3'], snacks: ['S8'], fish_seafood: ['T0', 'T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'T8', 'T9', 'Y6'], meat: ['U0', 'U1', 'U2', 'U3', 'U4', 'U5', 'U6', 'U7', 'U8', 'U9', 'V0', 'V1', 'V2', 'V5', 'W0', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W8', 'Y0', 'Y1', 'Y2', 'Y3', 'Y4'], poultry: ['V3', 'V4', 'V6', 'V8', 'V9', 'Y5'], soups: ['X4'], mixed_dishes: ['X0', 'X1', 'X2', 'Y8'], unmapped: [ 'H8', // tofu, miso and fruit-and-nut mix 'Q5', 'Q9', // coconut oil beside plant cheese; tahini beside plant lard 'R4', 'R5', 'R9', // additives and instant powders, creams and fillings 'W9', // pâtés in pastry, meat and vegetarian 'X9', // porridge and pancakes 'Y7', // egg dishes beside fish spreads 'Y9', // desserts beside savoury dishes ], }); export const blsGroup = (code: string | null | undefined): GroupEntry => lookup(blsGroups, code?.slice(0, 2)); // ---- Frida 5.5 (Denmark) --------------------------------------------------------------------- // FoodGroupID of the Frida food sheet (names from its FoodGroup sheet in the comments). Groups 6 and // 135 are infant foods and are skipped by the importer before this table is read (C-23). export const fridaGroups = table('frida', { dairy: ['4', '5', '8', '12', '13', '14', '15', '16', '17', '215'], // milk, fermented milk, cheeses, whey sweets_desserts: ['21', '23', '32', '33', '101', '102', '103', '104', '132', '182', '185', '199'], // ice cream, sorbet, biscuits, cakes, sugar, honey, chocolate, candy, jam grains_cereals: ['28', '30', '34', '36'], // grain and groats, flour, breakfast products, starch bread_bakery: ['31'], pasta_rice: ['35'], vegetables: ['39', '40', '41', '43', '44', '46', '187', '188', '192'], // root, leaf, fruit vegetables, mushrooms, herbs, canned, seaweed, frozen legumes: ['171', '172', '173', '174', '175', '191'], fruit: ['49', '50', '51', '52', '55', '184', '193'], drinks: ['45', '54', '106', '107', '111', '117', '186'], // vegetable and fruit juices, other beverages, coffee and tea, water nuts_seeds: ['53', '177', '178', '189'], meat: ['57', '58', '59', '60', '61', '63', '64', '65', '66', '69'], poultry: ['81', '82', '83', '84'], fish_seafood: ['70', '72', '73', '76', '77', '78', '79', '183'], eggs: ['87', '88', '89', '90'], fats_oils: ['93', '94', '95', '96'], sauces_condiments: ['97', '98', '120', '121', '122', '124', '125', '133', '181', '200'], alcohol: ['112', '113', '114', '115', '116', '194'], plant_alternatives: ['168', '169'], mixed_dishes: ['127', '128', '130', '131', '179'], soups: ['180'], snacks: ['143'], }); export const fridaGroup = (id: string | null | undefined): GroupEntry => lookup(fridaGroups, id); // ---- Matvaretabellen 2026 (Norway) ----------------------------------------------------------- // foodGroupId of food-groups-en.json. Group 11 (infant food) is skipped by the importer (C-23). export const matvaretabellenGroups = table('matvaretabellen', { dairy: ['1.1', '1.2', '1.3', '1.4.4', '1.4.5', '1.4.6', '1.4.7', '1.4.8', '1.4.9'], supplement: ['10.10'], // food supplements (C-26) snacks: ['10.5'], mixed_dishes: ['10.9.1', '10.9.2', '10.9.3', '3.7', '4.2'], legumes: ['12'], fruit: ['13.1', '13.2'], nuts_seeds: ['14.1', '14.2', '14.3'], vegetables: ['15.1', '15.2', '6.2', '6.6'], sauces_condiments: ['16'], eggs: ['2'], poultry: ['3.1'], meat: ['3.10', '3.2', '3.3', '3.4', '3.5', '3.8', '3.9'], fish_seafood: ['4.1.1', '4.1.2', '4.1.5', '4.3', '4.4'], sweets_desserts: ['5.10', '5.8', '7.1', '7.2', '7.3', '7.4'], grains_cereals: ['5.12', '5.1', '5.3'], bread_bakery: ['5.5.1', '5.5.2', '5.5.3', '5.6.1', '5.6.2', '5.6.3', '5.6.4', '5.6.5', '5.7', '5.9'], fats_oils: ['8.1', '8.2', '8.5'], drinks: ['9.1', '9.2', '9.4', '9.6'], alcohol: ['9.3'], plant_alternatives: ['9.5'], unmapped: [ '10.11', // dinner products: vinegar, bouillon, ready sauces '10.6', // vegetarian products and dishes: spreads, purées, meat alternatives '10.7', // miscellaneous ingredients '10.9.4', // soups and casseroles '13.3', // fruit and berry products: jam beside dried fruit '5.2', // grain, rice and pasta '8.3', // mayonnaise and mayonnaise salads ], }); export const matvaretabellenGroup = (id: string | null | undefined): GroupEntry => lookup(matvaretabellenGroups, id); // ---- TFDA (Taiwan), BLV (Switzerland), MEXT (Japan), TACO (Brazil) ----------------------------- // TFDA 食品分類. 加工調理食品及其他類 mixes shellfish, prepared dishes and condiments, so it stays undecided. export const tfdaGroups = table('tfda', { dairy: ['乳品類'], nuts_seeds: ['堅果及種子類'], fruit: ['水果類'], fats_oils: ['油脂類'], vegetables: ['澱粉類', '菇類', '蔬菜類', '藻類'], // starchy roots, mushrooms, vegetables, algae grains_cereals: ['穀物類'], sweets_desserts: ['糕餅點心類', '糖類'], meat: ['肉類'], eggs: ['蛋類'], sauces_condiments: ['調味料及香辛料類'], legumes: ['豆類'], drinks: ['飲料類'], fish_seafood: ['魚貝類'], unmapped: ['加工調理食品及其他類'], }); export const tfdaGroup = (category: string | null | undefined): GroupEntry => lookup(tfdaGroups, category); // BLV "Kategorie": "Hauptgruppe/Untergruppe", several separated by ";" (the first decides). A sub-group // row decides, then the main group. export const blvGroups = table('blv', { drinks: ['Alkoholfreie Getränke', 'Früchte/Fruchtsäfte'], alcohol: ['Alkoholhaltige Getränke'], eggs: ['Eier'], fish_seafood: ['Fisch'], meat: ['Fleisch und Innereien', 'Fleisch- und Wurstwaren'], poultry: ['Fleisch und Innereien/Geflügel'], fruit: ['Früchte'], vegetables: ['Gemüse', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Kartoffeln und andere stärkereiche Knollen'], nuts_seeds: ['Nüsse, Samen und Ölfrüchte'], plant_alternatives: ['Pflanzliche Proteinlieferanten und Alternativen zu tierischen Produkten'], snacks: ['Salzige Snacks'], sweets_desserts: ['Süssigkeiten', 'Gerichte/Sonstige süsse Gerichte'], mixed_dishes: ['Gerichte'], soups: ['Gerichte/Eintöpfe und Suppen'], dairy: ['Milch und Milchprodukte', 'Fette und Öle/Rahm'], fats_oils: ['Fette und Öle'], sauces_condiments: ['Fette und Öle/Mayonnaisen', 'Fette und Öle/Salatsaucen', 'Verschiedenes/Salz, Gewürze und Aromen', 'Verschiedenes/Saucen'], bread_bakery: ['Brote, Flocken und Frühstückscerealien/Brote und Brotwaren', 'Brote, Flocken und Frühstückscerealien/Knäckebrote, Zwieback, Crackers und Waffeln', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Teige'], grains_cereals: ['Brote, Flocken und Frühstückscerealien/Flocken, Kleie und Keime', 'Brote, Flocken und Frühstückscerealien/Müeslimischungen und Frühstückscerealien', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Mais', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Mehle und Stärke', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Sonstige Getreideprodukte'], pasta_rice: ['Getreideprodukte, Hülsenfrüchte und Kartoffeln/Reis', 'Getreideprodukte, Hülsenfrüchte und Kartoffeln/Teigwaren'], legumes: ['Getreideprodukte, Hülsenfrüchte und Kartoffeln/Hülsenfrüchte'], unmapped: [ 'Gerichte/Müesli und Brei', // porridge beside muesli dishes 'Verschiedenes/Aufstriche', 'Verschiedenes/Gelier- und Bindemittel', 'Verschiedenes/Hefe', // not foods eaten on their own ], }); export function blvGroup(category: string | null | undefined): GroupEntry { const first = category?.split(';')[0]?.trim(); if (!first) return null; return blvGroups.has(first) ? blvGroups.get(first)! : lookup(blvGroups, first.split('/')[0]); } // MEXT: the chapter (first two digits of the food number) decides unless the name's own leading // <category> or (subcategory) has a row. Chapter 01 holds rice, bread and noodles together; it is // searchable as cereals and does not claim to be one group for matching. export const mextGroups = table('mext', { grains_cereals: ['01', '02<でん粉・でん粉製品>'], vegetables: ['02', '06', '08', '09'], sweets_desserts: ['03', '15', '13<牛乳及び乳製品>(アイスクリーム類)'], legumes: ['04'], nuts_seeds: ['05'], fruit: ['07'], fish_seafood: ['10'], meat: ['11'], poultry: ['11<鳥肉類>'], eggs: ['12'], dairy: ['13'], fats_oils: ['14'], drinks: ['16'], alcohol: ['16<アルコール飲料類>'], sauces_condiments: ['17'], mixed_dishes: ['18'], snacks: ['15<スナック類>'], bread_bakery: ['15<菓子パン類>'], unmapped: [ '11<その他>', // insects (tsukudani) '17<その他>', // yeast ], }); export function mextGroup(chapter: string | null | undefined, name: string): GroupEntry { if (!chapter) return null; const bracket = name.match(/^<[^>]*>/)?.[0] ?? ''; const paren = name.replace(/^<[^>]*>\s*/, '').match(/^([^)]*)/)?.[0] ?? ''; for (const key of [`${chapter}${bracket}${paren}`, `${chapter}${bracket}`, chapter]) if (mextGroups.has(key)) return mextGroups.get(key)!; return null; } // TACO "grupo" (the section headings of the book). Mixed groups stay undecided. export const tacoGroups = table('taco', { mixed_dishes: ['Alimentos preparados'], meat: ['Carnes e derivados'], grains_cereals: ['Cereais e derivados'], fruit: ['Frutas e derivados', 'Frutos e derivados'], fats_oils: ['Gorduras e óleos'], legumes: ['Leguminosas e derivados'], dairy: ['Leite e derivados'], nuts_seeds: ['Nozes e sementes'], eggs: ['Ovos e derivados'], fish_seafood: ['Pescados e frutos do mar'], sweets_desserts: ['Produtos açucarados'], vegetables: ['Verduras, hortaliças e derivados'], unmapped: [ 'Bebidas (alcoólicas e não alcoólicas)', // soft drinks, coffee and spirits 'Miscelâneas', // coffee powder, instant mixes, condiments 'Outros alimentos industrializados', // olives, soups, sauces and more ], }); export const tacoGroup = (group: string | null | undefined): GroupEntry => lookup(tacoGroups, group); // Oxford 2024 chain menus (SRC-07): the dataset's own "Subcategory". A dish takes the group of its make-up // (a burger or a pizza is a mixed dish, a salad is vegetables). A subcategory not listed stays undecided. export const oxfordGroups = table('oxford', { mixed_dishes: ['Pizzas', 'Sandwiches', 'Burgers', 'Other Mains', 'Breakfast Items', "Children's Meals", 'Other Sides'], poultry: ['Chicken'], vegetables: ['Salads', 'Potato Sides'], sweets_desserts: ['Desserts'], sauces_condiments: ['Sauces'], }); export const oxfordGroup = (subcategory: string | null | undefined): GroupEntry => lookup(oxfordGroups, subcategory); // ---- Fineli (Finland, THL) --------------------------------------------------------------------- // FUCLASS (food use class) of food.csv. Infant foods and formulas (C-23) and supplements (C-26) are not in the base. export const fineliGroups = table('fineli', { fruit: ['FRUFRESH', 'BERFRESH', 'FRUBSAL'], vegetables: ['VEGFRESH', 'VEGPOT', 'VEGCANN', 'POTACOOK'], drinks: ['FRUBJUIC', 'VEGJUICE', 'COFFEE', 'TEA', 'DRWATER', 'DRJUICE', 'DRINKSO', 'DRINKCO', 'DRINKART', 'DRSPORT'], sweets_desserts: ['FRUBSOUP', 'FRUBPAST', 'JAM', 'BAKSWEET', 'BISCUIT', 'ICECREAM', 'MILKPUDD', 'SUGADD', 'SWEET', 'CHOCOL'], mixed_dishes: ['VEGDISH', 'VEGCASS', 'MUSHRDI', 'SALADVEG', 'SALADMIX', 'POTADISH', 'POTAFAT', 'SANDWICH', 'PIZZA', 'EGGMIX', 'FISHCASS', 'FISHDOTH', 'MINCMED', 'MEATPOT', 'MEATCASS', 'SAUSDISH', 'OFFALDI'], soups: ['VEGSOUP', 'FISHSOUP', 'MEATSOUP'], bread_bakery: ['BRMIX', 'BRWHITE', 'BRRYE', 'BUN', 'BAKSALT', 'CERPR'], pasta_rice: ['RICEADD', 'PASTAADD'], grains_cereals: ['PORR', 'CERBRKF'], dairy: ['MILKFF', 'MILKLF', 'MILKHF', 'SMILK', 'YOGHURT', 'MILKCURL', 'CHEESHAR', 'CHEESOFT', 'VEGCHEES'], sauces_condiments: ['MILKSAUC', 'DRESSING', 'FATSAUCE', 'SPICES', 'SPISAUCE'], fats_oils: ['BRFBUTT', 'BRFMARG', 'BRFLOWF', 'FATPROD'], eggs: ['EGGS'], fish_seafood: ['FISH', 'FISHPROD'], meat: ['MSTEAK', 'SAUSAGE'], poultry: ['POULTDI'], alcohol: ['BEER', 'WINE', 'SPIRIT', 'ALCOTH'], snacks: ['SNACK'], infant: ['BABMEATD', 'BABFISHD', 'BABMILPO', 'BABWATPO', 'BABFRUB', 'BABVEGE', 'BABMIFRU', 'BABOTHER', 'MMILK', 'INFMILK', 'CASMILK', 'PREMILK', 'SOYMILK', 'WHEYMILK', 'AMINMILK'], supplement: ['SPECSUPP'], unmapped: [ 'INGRMISC', // miscellaneous ingredients: flours, starches and gelatine beside pulses 'MEALREP', 'SPECFOOD', // meal replacements and clinical nutrition products ], }); // The ingredient class places a plain ingredient that its use class files with dishes or drinks: raw meat in a minced-meat // dish class, soy and oat drinks under vegetable juices, nuts under snacks. export function fineliGroup(food: { fuclass?: string | null; igclass?: string | null; igclassp?: string | null; foodtype?: string | null }): GroupEntry { if (food.foodtype === 'SUPPLE') return 'supplement'; const use = lookup(fineliGroups, food.fuclass); if (isExcluded(use)) return use; if (food.igclass === 'NUTSEED') return 'nuts_seeds'; if (['VEGJUICE', 'MILKHF', 'MILKLF', 'YOGHURT'].includes(food.fuclass ?? '') && ['CERTOT', 'LEGUTOT'].includes(food.igclassp ?? '')) return 'plant_alternatives'; if (food.igclassp === 'LEGUTOT' && ['VEGFRESH', 'VEGCANN', 'VEGPOT', 'INGRMISC'].includes(food.fuclass ?? '')) return 'legumes'; if (food.igclassp === 'MEATTOT' && use === 'mixed_dishes') return 'meat'; if (food.igclassp === 'FISHTOT' && use === 'mixed_dishes') return 'fish_seafood'; return use; }