# -*- coding: utf-8 -*- """ Helper module for ingredient parsing, table structuring, and quantity scaling. """ import re from typing import List, Dict, Any, Optional COMMON_UNITS = [ "kg", "g", "mg", "l", "liter", "ml", "cl", "el", "tl", "esslöffel", "teelöffel", "pck.", "pck", "päckchen", "pkt.", "pkt", "dose", "dosen", "glas", "gläser", "prise", "prisen", "msp.", "msp", "messerspitze", "bund", "stk.", "stk", "stück", "stücke", "zehe", "zehen", "zweig", "zweige", "blatt", "blätter", "scheibe", "scheiben", "stange", "stangen", "kopf", "nest", "nester", "tropfen", "tr." ] def parse_fraction(val_str: str) -> Optional[float]: val_str = val_str.strip() if "/" in val_str: parts = val_str.split("/") if len(parts) == 2: try: return float(parts[0]) / float(parts[1]) except ValueError: pass try: return float(val_str.replace(",", ".")) except ValueError: return None def parse_ingredient_line(line: str) -> Dict[str, Any]: raw = line.strip() if not raw: return {"type": "empty", "raw": ""} # Check if section header (ends with colon or keywords) lower = raw.lower() if raw.endswith(":") or lower.startswith("für ") or lower in [ "teig", "sauce", "soße", "füllung", "belag", "creme", "glasur", "marinade", "deko", "schichten & deko", "keksschicht", "heidelbeersoße", "zitronen-zimt-sirup", "zutaten", "einlage & toppings", "gemüse & toppings", "lemon curd", "mascarponecreme" ]: hdr_text = raw.rstrip(":") return { "type": "header", "is_header": True, "text": hdr_text, "name": hdr_text, "raw": raw } # Regex to match leading amount match = re.match(r"^(\d+\s+\d+/\d+|\d+/\d+|\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*(.*)$", raw) if match: amt_str = match.group(1).strip() range_end = match.group(2) rest = match.group(3).strip() if " " in amt_str and "/" in amt_str: whole, frac = amt_str.split() val = float(whole) + (parse_fraction(frac) or 0.0) elif "/" in amt_str: val = parse_fraction(amt_str) or 0.0 else: val = parse_fraction(amt_str) or 0.0 display_amt = f"{amt_str}-{range_end}" if range_end else amt_str rest_parts = rest.split(None, 1) if rest_parts: first_word_clean = rest_parts[0].lower().rstrip(".,") if first_word_clean in COMMON_UNITS or rest_parts[0].lower() in COMMON_UNITS: unit = rest_parts[0] ing_name = rest_parts[1] if len(rest_parts) > 1 else "" return { "type": "ingredient", "is_header": False, "amount_val": val, "amount": val, "amount_str": display_amt, "amount_formatted": display_amt, "unit": unit, "name": ing_name, "raw": raw } return { "type": "ingredient", "is_header": False, "amount_val": val, "amount": val, "amount_str": display_amt, "amount_formatted": display_amt, "unit": "", "name": rest, "raw": raw } return { "type": "ingredient", "is_header": False, "amount_val": None, "amount": None, "amount_str": "", "amount_formatted": "", "unit": "", "name": raw, "raw": raw } def parse_ingredients_block(text: str) -> List[Dict[str, Any]]: lines = [l.strip() for l in (text or "").split("\n") if l.strip()] return [parse_ingredient_line(l) for l in lines] def format_quantity_py(num: Optional[float]) -> str: """Formatiert eine Zahl lesbar für Rezepte (z. B. 0.5 -> ½, 1.5 -> 1 ½, 2.0 -> 2).""" if num is None or num <= 0: return "" whole = int(num) frac = round((num - whole) * 100) / 100.0 frac_str = "" if 0.20 <= frac <= 0.30: frac_str = "¼" elif 0.31 <= frac <= 0.40: frac_str = "⅓" elif 0.45 <= frac <= 0.55: frac_str = "½" elif 0.63 <= frac <= 0.70: frac_str = "⅔" elif 0.72 <= frac <= 0.80: frac_str = "¾" if frac_str: return f"{whole} {frac_str}".strip() if whole > 0 else frac_str if abs(num - round(num)) < 0.05: return str(int(round(num))) # Deutsche Kommaschreibweise val_str = f"{num:.1f}".rstrip("0").rstrip(".").replace(".", ",") return val_str def calculate_portion_matrix( parsed_ingredients: List[Dict[str, Any]], base_portions: int = 4, targets: tuple = (2, 4, 6, 8) ) -> List[Dict[str, Any]]: """Berechnet für Waffeln / Pancakes etc. eine vorberechnete Tabelle für 2, 4, 6, 8 Portionen.""" if not base_portions or base_portions <= 0: base_portions = 4 matrix = [] for item in parsed_ingredients: if item.get("is_header"): matrix.append({ "is_header": True, "text": item.get("text") or item.get("name") or "", "amounts": {p: "" for p in targets} }) continue row_amounts = {} for p in targets: amt_val = item.get("amount_val") unit = item.get("unit") or "" if amt_val is not None: scaled_val = amt_val * (p / float(base_portions)) formatted_amt = format_quantity_py(scaled_val) row_amounts[p] = f"{formatted_amt} {unit}".strip() else: row_amounts[p] = "—" matrix.append({ "is_header": False, "name": item.get("name") or item.get("raw") or "", "raw": item.get("raw", ""), "amounts": row_amounts }) return matrix