""" Health Data Validators Smart parsing and validation for health metrics with multiple input formats """ import re from typing import Optional, Union, Tuple class HealthDataParser: """Parse health data from various input formats""" @staticmethod def parse_height(value: Union[str, int, float]) -> Optional[float]: """ Parse height from various formats to cm Supports: - 1.78m, 1.78 m → 178 cm - 178cm, 178 cm → 178 cm - 1,78m (comma) → 178 cm - 178 → 178 cm - 5'10" → 177.8 cm (feet/inches) Args: value: Height in various formats Returns: Height in cm or None if invalid """ if value is None: return None # Convert to string and normalize value_str = str(value).strip().lower().replace(',', '.') # Remove spaces value_str = value_str.replace(' ', '') # Pattern 1: Meters (1.78m, 1.78) meter_match = re.match(r'^(\d+\.?\d*)m?$', value_str) if meter_match: meters = float(meter_match.group(1)) # If value is between 0.5 and 3.0, assume it's in meters if 0.5 <= meters <= 3.0: return round(meters * 100, 1) # If value is > 50, assume it's already in cm elif meters >= 50: return round(meters, 1) # Pattern 2: Centimeters (178cm, 178) cm_match = re.match(r'^(\d+\.?\d*)cm?$', value_str) if cm_match: cm = float(cm_match.group(1)) if 50 <= cm <= 300: return round(cm, 1) # Pattern 3: Feet and inches (5'10", 5ft10in) feet_match = re.match(r'^(\d+)[\'ft](\d+)[\"in]?$', value_str) if feet_match: feet = int(feet_match.group(1)) inches = int(feet_match.group(2)) total_inches = feet * 12 + inches cm = total_inches * 2.54 return round(cm, 1) # Try direct float conversion try: num = float(value_str) # If between 0.5 and 3.0, assume meters if 0.5 <= num <= 3.0: return round(num * 100, 1) # If between 50 and 300, assume cm elif 50 <= num <= 300: return round(num, 1) except ValueError: pass return None @staticmethod def parse_weight(value: Union[str, int, float]) -> Optional[float]: """ Parse weight from various formats to kg Supports: - 70kg, 70 kg → 70 kg - 70, 70.5 → 70 kg - 154lbs, 154 lbs → 69.9 kg - 11st 2lb → 70.8 kg (stones) Args: value: Weight in various formats Returns: Weight in kg or None if invalid """ if value is None: return None value_str = str(value).strip().lower().replace(',', '.') value_str = value_str.replace(' ', '') # Pattern 1: Kilograms (70kg, 70) kg_match = re.match(r'^(\d+\.?\d*)kg?$', value_str) if kg_match: kg = float(kg_match.group(1)) if 20 <= kg <= 300: return round(kg, 1) # Pattern 2: Pounds (154lbs, 154lb) lbs_match = re.match(r'^(\d+\.?\d*)lbs?$', value_str) if lbs_match: lbs = float(lbs_match.group(1)) kg = lbs * 0.453592 if 20 <= kg <= 300: return round(kg, 1) # Pattern 3: Stones (11st, 11stone) stone_match = re.match(r'^(\d+)st(?:one)?(\d+)?lbs?$', value_str) if stone_match: stones = int(stone_match.group(1)) lbs = int(stone_match.group(2)) if stone_match.group(2) else 0 total_lbs = stones * 14 + lbs kg = total_lbs * 0.453592 return round(kg, 1) # Try direct float conversion try: num = float(value_str) if 20 <= num <= 300: return round(num, 1) except ValueError: pass return None @staticmethod def parse_age(value: Union[str, int, float]) -> Optional[int]: """ Parse age from various formats Supports: - 25, "25" → 25 - "25 tuổi", "25 years old" → 25 Args: value: Age in various formats Returns: Age as integer or None if invalid """ if value is None: return None value_str = str(value).strip().lower() # Extract number from string age_match = re.search(r'(\d+)', value_str) if age_match: age = int(age_match.group(1)) if 0 <= age <= 150: return age return None @staticmethod def parse_bmi(value: Union[str, int, float]) -> Optional[float]: """Parse BMI value""" if value is None: return None try: bmi = float(str(value).strip()) if 10 <= bmi <= 60: return round(bmi, 1) except ValueError: pass return None class HealthDataValidator: """Validate health data for abnormal values""" @staticmethod def validate_height(height: float) -> Tuple[bool, Optional[str]]: """ Validate height in cm Returns: (is_valid, error_message) """ if height is None: return True, None if height < 50: return False, "Chiều cao quá thấp (< 50cm). Vui lòng kiểm tra lại." if height > 300: return False, "Chiều cao quá cao (> 300cm). Vui lòng kiểm tra lại." if height < 100: return False, f"Chiều cao {height}cm có vẻ không đúng. Bạn có muốn nhập {height*100}cm không?" return True, None @staticmethod def validate_weight(weight: float) -> Tuple[bool, Optional[str]]: """ Validate weight in kg Returns: (is_valid, error_message) """ if weight is None: return True, None if weight < 20: return False, "Cân nặng quá nhẹ (< 20kg). Vui lòng kiểm tra lại." if weight > 300: return False, "Cân nặng quá nặng (> 300kg). Vui lòng kiểm tra lại." return True, None @staticmethod def validate_age(age: int) -> Tuple[bool, Optional[str]]: """ Validate age Returns: (is_valid, error_message) """ if age is None: return True, None if age < 0: return False, "Tuổi không thể âm." if age > 150: return False, "Tuổi quá cao (> 150). Vui lòng kiểm tra lại." if age < 13: return False, "Hệ thống chỉ hỗ trợ người từ 13 tuổi trở lên." return True, None @staticmethod def validate_bmi(bmi: float) -> Tuple[bool, Optional[str]]: """ Validate BMI Returns: (is_valid, error_message) """ if bmi is None: return True, None if bmi < 10: return False, "BMI quá thấp (< 10). Vui lòng kiểm tra lại." if bmi > 60: return False, "BMI quá cao (> 60). Vui lòng kiểm tra lại." return True, None @staticmethod def calculate_bmi(weight: Optional[float], height: Optional[float]) -> Optional[float]: """ Calculate BMI from weight (kg) and height (cm) Returns: BMI or None if data is missing """ if weight is None or height is None: return None if height <= 0 or weight <= 0: return None # Convert height from cm to meters height_m = height / 100 bmi = weight / (height_m ** 2) return round(bmi, 1) @staticmethod def get_bmi_category(bmi: Optional[float]) -> str: """Get BMI category (Vietnamese)""" if bmi is None: return "Chưa xác định" if bmi < 18.5: return "Thiếu cân" elif bmi < 23: # Asian BMI standards return "Bình thường" elif bmi < 25: return "Thừa cân nhẹ" elif bmi < 30: return "Thừa cân" else: return "Béo phì"