Spaces:
Runtime error
Runtime error
| """ | |
| 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""" | |
| 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 | |
| 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 | |
| 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 | |
| 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""" | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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) | |
| 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ì" | |