from __future__ import annotations from typing import Any, Mapping from app.modules.zootech.lab.nutrient_schema import resolve_sv_g_per_kg def normalize_key(value: str) -> str: return " ".join((value or "").split()).strip().lower() def parse_num(value: Any) -> float | None: if value is None or value == "": return None try: n = float(value) except (TypeError, ValueError): return None return None if n != n else n def get_nutrient_value( dry_matter: float | None, nutrients: Mapping[str, Any] | None, nutrient_keys: list[str], *, indicator_key: str | None = None, ) -> float | None: """Только точное совпадение ключа (заголовок или indicator_key slug).""" nutrients = nutrients or {} if indicator_key: for k, v in nutrients.items(): if normalize_key(str(k)) == normalize_key(indicator_key): n = parse_num(v) if n is not None: return n for search in nutrient_keys: target = normalize_key(search) for k, v in nutrients.items(): if normalize_key(str(k)) != target: continue n = parse_num(v) if n is not None: return n if any(normalize_key(k) == "св" for k in nutrient_keys): return resolve_sv_g_per_kg(nutrients, dry_matter) return None def weighted_average( lines: list[dict[str, Any]], total_kg: float, nutrient_keys: list[str], *, indicator_key: str | None = None, ) -> float | None: if total_kg <= 0: return None total = 0.0 weight = 0.0 for line in lines: kg = float(line.get("daily_kg") or 0) if kg <= 0: continue v = get_nutrient_value( line.get("dry_matter"), line.get("nutrients"), nutrient_keys, indicator_key=indicator_key, ) if v is None: continue total += kg * v weight += kg if weight <= 0: return None return total / weight def daily_intake_total( lines: list[dict[str, Any]], nutrient_keys: list[str], *, heads_per_trip: int = 1, indicator_key: str | None = None, ) -> float | None: """Суточная доза на голову (г или МДж): Σ (кг/день/гол × г/кг). Как Excel Рацион КРС.""" heads = max(int(heads_per_trip or 1), 1) total = 0.0 any_value = False for line in lines: herd_kg = float(line.get("daily_kg") or 0) if herd_kg <= 0: continue kg = herd_kg / heads v = get_nutrient_value( line.get("dry_matter"), line.get("nutrients"), nutrient_keys, indicator_key=indicator_key, ) if v is None: continue total += kg * v any_value = True return total if any_value else None def rnb( crude_protein: float | None, usp: float | None, ) -> float | None: """RNB (ruminal nitrogen balance, GfE): (сырой протеин − уСП) / 6,25.""" if crude_protein is None or usp is None: return None return (crude_protein - usp) / 6.25 def bra_rnb(crude_protein: float | None, usp: float | None) -> float | None: """Deprecated alias for :func:`rnb`.""" return rnb(crude_protein, usp) def norm_diff( content: float | None, min_val: float | None, max_val: float | None, ) -> float | None: if content is None: return None if min_val is not None and content < min_val: return content - min_val if max_val is not None and content > max_val: return content - max_val return 0.0