129 lines
3.4 KiB
Python
129 lines
3.4 KiB
Python
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
|