import math from typing import Any def _is_female(sex: str) -> bool: return sex.lower() in ("f", "female", "ж", "женский", "woman") def _cm_to_inches(cm: float) -> float: return cm / 2.54 def _clamp_bf(value: float) -> float: return round(max(3.0, min(50.0, value)), 1) def navy_body_fat_pct( *, sex: str, height_cm: float, neck_cm: float, waist_cm: float, hip_cm: float | None = None, ) -> float | None: if height_cm <= 0 or neck_cm <= 0 or waist_cm <= 0: return None height_in = _cm_to_inches(height_cm) neck_in = _cm_to_inches(neck_cm) waist_in = _cm_to_inches(waist_cm) if _is_female(sex): if hip_cm is None or hip_cm <= 0: return None hip_in = _cm_to_inches(hip_cm) sum_in = waist_in + hip_in - neck_in if sum_in <= 0: return None denom = ( 1.29579 - 0.35004 * math.log10(sum_in) + 0.22100 * math.log10(height_in) ) else: diff_in = waist_in - neck_in if diff_in <= 0: return None denom = ( 1.0324 - 0.19077 * math.log10(diff_in) + 0.15456 * math.log10(height_in) ) if denom <= 0: return None return _clamp_bf(495.0 / denom - 450.0) def whr(waist_cm: float, hip_cm: float) -> float | None: if waist_cm <= 0 or hip_cm <= 0: return None return round(waist_cm / hip_cm, 2) def lean_body_mass(weight_kg: float, body_fat_pct: float) -> float: return round(weight_kg * (1.0 - body_fat_pct / 100.0), 1) def ffmi(weight_kg: float, height_cm: float, body_fat_pct: float) -> float | None: if height_cm <= 0: return None height_m = height_cm / 100.0 lbm = weight_kg * (1.0 - body_fat_pct / 100.0) raw = lbm / (height_m * height_m) normalized = raw + 6.1 * (1.8 - height_m) return round(normalized, 1) def compute_body_composition( *, sex: str, height_cm: float, weight_kg: float, neck_cm: float | None = None, waist_cm: float | None = None, hip_cm: float | None = None, body_fat_pct: float | None = None, ) -> dict[str, Any]: warnings: list[str] = [] result: dict[str, Any] = { "body_fat_pct": None, "body_fat_method": None, "whr": None, "lbm_kg": None, "ffmi": None, "warnings": warnings, } bf = body_fat_pct method: str | None = "manual" if bf is not None else None if bf is None and neck_cm and waist_cm: navy_bf = navy_body_fat_pct( sex=sex, height_cm=height_cm, neck_cm=neck_cm, waist_cm=waist_cm, hip_cm=hip_cm, ) if navy_bf is not None: bf = navy_bf method = "navy" elif _is_female(sex) and not hip_cm: warnings.append("Для Navy у женщин нужен обхват бёдер (hip_cm).") elif neck_cm and waist_cm and waist_cm <= neck_cm: warnings.append("Обхват талии должен быть больше шеи для Navy.") if bf is not None: result["body_fat_pct"] = round(float(bf), 1) result["body_fat_method"] = method result["lbm_kg"] = lean_body_mass(weight_kg, float(bf)) result["ffmi"] = ffmi(weight_kg, height_cm, float(bf)) if waist_cm and hip_cm: result["whr"] = whr(waist_cm, hip_cm) return result