Initial commit: What To Cook (WTC) v4.4 - Digitales Kochbuch, Wochenmenueplan & Buchdruck
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# -*- 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
|
||||
|
||||
Reference in New Issue
Block a user