import { BASE_DAILY_TARGETS, LEVEL1_NUTRITION_KEYS, LEVEL2_NUTRITION_KEYS, LEVEL3_NUTRITION_KEYS, NUTRIENT_META, USA_GOV_DGA_KEYS, } from '@/common/constants/nutrition'; import type { Strings } from '@/common/strings/types'; import { NutrientKey, Nutrition, NutritionDisplayMode, NutritionSortMode } from '@/types/nutrition'; type NutrientStrings = Strings['nutrients']; type NutrientStringKey = keyof Omit; type FoodGroupStringKey = keyof NutrientStrings['foodGroups']; const NUTRIENT_STRING_MAP: Partial> = { protein_g: 'protein', carbs_g: 'carbs', fat_g: 'fat', fiber_g: 'fiber', soluble_fiber_g: 'solubleFiber', insoluble_fiber_g: 'insolubleFiber', a_mcg: 'vitaminA', retinol_mcg: 'retinol', b1_mg: 'thiamin', b2_mg: 'riboflavin', b3_mg: 'niacin', b5_mg: 'pantothenicAcid', b6_mg: 'vitaminB6', b9_mcg: 'folate', b12_mcg: 'vitaminB12', c_mg: 'vitaminC', vitamin_d_mcg: 'vitaminD', e_mg: 'vitaminE', k_mcg: 'vitaminK', calcium_mg: 'calcium', iron_mg: 'iron', magnesium_mg: 'magnesium', zinc_mg: 'zinc', potassium_mg: 'potassium', phosphorus_mg: 'phosphorus', selenium_mcg: 'selenium', chromium_mcg: 'chromium', molybdenum_mcg: 'molybdenum', iodine_mcg: 'iodine', omega_3_mg: 'omega3', omega_6_mg: 'omega6', ala_mg: 'ala', epa_mg: 'epa', dha_mg: 'dha', dpa_mg: 'dpa', biotin_mcg: 'biotin', probiotics_cfu: 'probiotics', copper_mg: 'copper', manganese_mg: 'manganese', choline_mg: 'choline', water_ml: 'water', caffeine_mg: 'caffeine', theobromine_mg: 'theobromine', taurine_mg: 'taurine', collagen_g: 'collagen', coq10_mg: 'coq10', curcumin_mg: 'curcumin', creatine_g: 'creatine', resveratrol_mg: 'resveratrol', astaxanthin_mcg: 'astaxanthin', lycopene_mcg: 'lycopene', lutein_zeaxanthin_mcg: 'luteinZeaxanthin', beta_carotene_mcg: 'betaCarotene', alpha_carotene_mcg: 'alphaCarotene', cryptoxanthin_mcg: 'cryptoxanthin', anthocyanins_mg: 'anthocyanins', monounsaturated_fat_g: 'monounsaturatedFat', polyunsaturated_fat_g: 'polyunsaturatedFat', phytosterols_mg: 'phytosterols', tryptophan_mg: 'tryptophan', tyrosine_mg: 'tyrosine', valine_g: 'valine', glutamine_g: 'glutamine', betaine_mg: 'betaine', kcal: 'calories', alcohol_g: 'alcohol', cholesterol_mg: 'cholesterol', saturated_fat_g: 'saturatedFat', sodium_mg: 'sodium', sugar_g: 'sugar', starch_g: 'starch', oxalate_mg: 'oxalate', chloride_mg: 'chloride', }; const FOOD_GROUP_STRING_MAP: Partial> = { usa_gov_dga_protein_g: 'proteinFoods', usa_gov_dga_healthy_fats_tsp_eq: 'healthyFats', usa_gov_dga_vegetables_cup_eq: 'vegetables', usa_gov_dga_fruits_cup_eq: 'fruits', usa_gov_dga_dairy_cup_eq: 'dairy', usa_gov_dga_whole_grains_oz_eq: 'wholeGrains', usa_gov_dga_dark_green_veg_cup_eq: 'darkGreenVegetables', usa_gov_dga_red_orange_veg_cup_eq: 'redOrangeVegetables', usa_gov_dga_beans_peas_lentils_veg_cup_eq: 'legumes', usa_gov_dga_starchy_veg_cup_eq: 'starchyVegetables', usa_gov_dga_other_veg_cup_eq: 'otherVegetables', }; export function getNutrientLabel(key: NutrientKey, nutrients: NutrientStrings): string { const nutrientKey = NUTRIENT_STRING_MAP[key]; if (nutrientKey) return nutrients[nutrientKey] as string; const foodGroupKey = FOOD_GROUP_STRING_MAP[key]; if (foodGroupKey) return nutrients.foodGroups[foodGroupKey]; return NUTRIENT_META[key].label; } export type NutrientsByLevel = { level1: NutrientKey[]; level2: NutrientKey[]; level3: NutrientKey[]; }; export function getNutrientsByMode(mode: NutritionDisplayMode): NutrientsByLevel { if (mode === 'usa_gov_dga') { return { level1: USA_GOV_DGA_KEYS, level2: [], level3: [] }; } return { level1: LEVEL1_NUTRITION_KEYS, level2: LEVEL2_NUTRITION_KEYS, level3: LEVEL3_NUTRITION_KEYS, }; } export function sortNutrients( keys: NutrientKey[], sortMode: NutritionSortMode, consumed?: Nutrition, target?: Nutrition ): NutrientKey[] { if (sortMode === 'default') { return [...keys].sort((a, b) => NUTRIENT_META[a].importance - NUTRIENT_META[b].importance); } if (!consumed || !target) { return [...keys].sort((a, b) => NUTRIENT_META[a].importance - NUTRIENT_META[b].importance); } return [...keys].sort((a, b) => { const fillA = (consumed[a] ?? 0) / (target[a] || 1); const fillB = (consumed[b] ?? 0) / (target[b] || 1); return fillA - fillB; }); } export function sumNutrition(items: Nutrition[]): Nutrition { const result: Nutrition = {}; for (const item of items) { for (const key of Object.keys(item) as NutrientKey[]) { const val = item[key]; if (val != null) { result[key] = (result[key] ?? 0) + val; } } } return result; } export function emptyNutrition(): Nutrition { return {}; } export function getDailyTargets(date: Date): Nutrition { const dayOfYear = Math.floor( (date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 ); const seed = dayOfYear * 7 + date.getFullYear(); const result: Nutrition = {}; for (const [key, base] of Object.entries(BASE_DAILY_TARGETS)) { const hash = ((seed * 31 + key.charCodeAt(0)) * 17) % 100; const variation = 0.9 + (hash / 100) * 0.2; result[key as NutrientKey] = Math.round(base * variation * 100) / 100; } return result; } const SCORE_NUTRIENTS: { key: NutrientKey; weight: number }[] = [ { key: 'protein_g', weight: 12 }, { key: 'fiber_g', weight: 10 }, { key: 'omega_3_mg', weight: 10 }, { key: 'vitamin_d_mcg', weight: 8 }, { key: 'b12_mcg', weight: 7 }, { key: 'iron_mg', weight: 7 }, { key: 'calcium_mg', weight: 7 }, { key: 'magnesium_mg', weight: 7 }, { key: 'choline_mg', weight: 6 }, { key: 'dha_mg', weight: 6 }, { key: 'epa_mg', weight: 6 }, { key: 'potassium_mg', weight: 5 }, { key: 'zinc_mg', weight: 5 }, { key: 'c_mg', weight: 5 }, { key: 'a_mcg', weight: 5 }, { key: 'b9_mcg', weight: 5 }, { key: 'selenium_mcg', weight: 5 }, { key: 'iodine_mcg', weight: 5 }, { key: 'b6_mg', weight: 4 }, { key: 'e_mg', weight: 4 }, { key: 'k_mcg', weight: 4 }, { key: 'copper_mg', weight: 4 }, { key: 'manganese_mg', weight: 4 }, { key: 'b1_mg', weight: 3 }, { key: 'b2_mg', weight: 3 }, { key: 'b3_mg', weight: 3 }, { key: 'b5_mg', weight: 3 }, { key: 'biotin_mcg', weight: 3 }, { key: 'phosphorus_mg', weight: 3 }, { key: 'chromium_mcg', weight: 3 }, { key: 'molybdenum_mcg', weight: 3 }, { key: 'ala_mg', weight: 3 }, { key: 'soluble_fiber_g', weight: 3 }, { key: 'insoluble_fiber_g', weight: 3 }, { key: 'retinol_mcg', weight: 2 }, { key: 'dpa_mg', weight: 2 }, { key: 'omega_6_mg', weight: 2 }, { key: 'monounsaturated_fat_g', weight: 2 }, { key: 'polyunsaturated_fat_g', weight: 2 }, { key: 'tryptophan_mg', weight: 2 }, { key: 'tyrosine_mg', weight: 2 }, { key: 'valine_g', weight: 2 }, { key: 'glutamine_g', weight: 2 }, { key: 'lycopene_mcg', weight: 2 }, { key: 'lutein_zeaxanthin_mcg', weight: 2 }, { key: 'beta_carotene_mcg', weight: 2 }, { key: 'alpha_carotene_mcg', weight: 1 }, { key: 'cryptoxanthin_mcg', weight: 1 }, { key: 'anthocyanins_mg', weight: 2 }, { key: 'phytosterols_mg', weight: 1 }, { key: 'betaine_mg', weight: 1 }, { key: 'probiotics_cfu', weight: 2 }, { key: 'collagen_g', weight: 1 }, { key: 'coq10_mg', weight: 1 }, { key: 'curcumin_mg', weight: 1 }, { key: 'creatine_g', weight: 1 }, { key: 'resveratrol_mg', weight: 1 }, { key: 'astaxanthin_mcg', weight: 1 }, { key: 'water_ml', weight: 2 }, { key: 'caffeine_mg', weight: 1 }, { key: 'theobromine_mg', weight: 1 }, { key: 'taurine_mg', weight: 1 }, ]; const SCORE_FOOD_GROUPS: { key: NutrientKey; weight: number }[] = [ { key: 'usa_gov_dga_vegetables_cup_eq', weight: 8 }, { key: 'usa_gov_dga_fruits_cup_eq', weight: 5 }, { key: 'usa_gov_dga_protein_g', weight: 5 }, { key: 'usa_gov_dga_whole_grains_oz_eq', weight: 4 }, { key: 'usa_gov_dga_dairy_cup_eq', weight: 3 }, { key: 'usa_gov_dga_healthy_fats_tsp_eq', weight: 3 }, { key: 'usa_gov_dga_dark_green_veg_cup_eq', weight: 4 }, { key: 'usa_gov_dga_red_orange_veg_cup_eq', weight: 3 }, { key: 'usa_gov_dga_beans_peas_lentils_veg_cup_eq', weight: 3 }, { key: 'usa_gov_dga_starchy_veg_cup_eq', weight: 2 }, { key: 'usa_gov_dga_other_veg_cup_eq', weight: 2 }, ]; function fillRatio(consumed: number, target: number): number { if (target <= 0) return 0; const ratio = consumed / target; return Math.min(ratio, 1.5) / 1.5; } export function calculateNutritionScore( consumed: Nutrition, targets: Nutrition, uniqueIngredientCount: number ): number { const nutrientTotal = SCORE_NUTRIENTS.reduce((sum, n) => sum + n.weight, 0); const foodGroupTotal = SCORE_FOOD_GROUPS.reduce((sum, n) => sum + n.weight, 0); let nutrientScore = 0; for (const { key, weight } of SCORE_NUTRIENTS) { const c = consumed[key] ?? 0; const t = targets[key] ?? BASE_DAILY_TARGETS[key] ?? 1; nutrientScore += fillRatio(c, t) * weight; } const nutrientPct = nutrientScore / nutrientTotal; let foodGroupScore = 0; for (const { key, weight } of SCORE_FOOD_GROUPS) { const c = consumed[key] ?? 0; const t = targets[key] ?? BASE_DAILY_TARGETS[key] ?? 1; foodGroupScore += fillRatio(c, t) * weight; } const foodGroupPct = foodGroupScore / foodGroupTotal; const diversityBonus = Math.min(uniqueIngredientCount / 15, 1); const rawScore = nutrientPct * 45 + foodGroupPct * 45 + diversityBonus * 10; return Math.min(Math.max(rawScore, 0), 100); }