"""نام‌گذاری متریال — تفکیک «خانواده» از «مشخصه/واریانت» به‌صورت عمومی. برخی ردیف‌های کاتالوگ، سایز/گرید را داخل نام ماده دارند («بلوک سیمانی ۲۰×۲۰×۴۰»، «لوله پلی‌اتیلن PE100 سایز ۲۵»). این ماژول با قواعد الگویی (ابعاد NxNxN، کلیدواژه‌های مشخصه، کدهای گرید لاتین) نام را به «خانواده» و «واریانت» می‌شکند — بدون هیچ هاردکدِ نام محصول مشخصی. """ import re _DIGIT_MAP = str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩", "01234567890123456789") # واژه‌هایی که شروع بخش مشخصه را نشان می‌دهند (ابعاد/درجه/تیپ/مدل/…) _ATTRIBUTE_WORDS = { "سایز", "تیپ", "مدل", "درجه", "ضخامت", "قطر", "ابعاد", "طول", "عرض", "ارتفاع", "رنگ", "وزن", "ظرفیت", "شماره", "کلاس", } # ابعاد ترکیبی: ۲۰×۲۰×۴۰ / ۲×۲٫۵ / 60x60 / 2*2.5 _DIMENSION_RE = re.compile(r"^\d+(?:[×xX*]\d+(?:[.,٫/]\d+)?)+$") # کد گرید: PE100 / PN10 / DN50 / A3 / SN4 / ST37 _GRADE_RE = re.compile(r"^(?:PN|PE|DN|SN|SCH|ST|A|B|C)[-]?\d+$") _NUMBER_RE = re.compile(r"^\d+(?:[.,٫/]\d+)?$") _LATIN_WORD_RE = re.compile(r"^[A-Za-z]{2,6}$") STANDARD_VARIANT_LABEL = "استاندارد" def normalize_digits(text: str) -> str: return (text or "").translate(_DIGIT_MAP) def _is_spec_start(tokens: list[str], index: int) -> bool: token = tokens[index] normalized = normalize_digits(token) if normalized in _ATTRIBUTE_WORDS: # «سایز»، «تیپ»، «مدل»، … return True if _DIMENSION_RE.match(normalized): # ۲۰×۲۰×۴۰ return True if _GRADE_RE.match(token): # PE100 / PN10 / A3 return True # کد لاتین کوتاه که پشتش عدد می‌آید: «IPE ۱۴» if ( _LATIN_WORD_RE.match(token) and index + 1 < len(tokens) and _NUMBER_RE.match(normalize_digits(tokens[index + 1])) ): return True # عددِ تنها در انتهای نام (بدون کلیدواژه): «لوله مسی ۱۵» if _NUMBER_RE.match(normalized) and index == len(tokens) - 1 and index >= 1: return True return False def split_material_name(name: str) -> tuple[str, str | None]: """«لوله پلی‌اتیلن PE100 سایز ۲۵» → («لوله پلی‌اتیلن», «PE100 سایز ۲۵»).""" tokens = [token for token in (name or "").split() if token] for index in range(1, len(tokens)): if _is_spec_start(tokens, index): family = " ".join(tokens[:index]).strip() variant = " ".join(tokens[index:]).strip() if family and variant: return family, variant return " ".join(tokens).strip(), None def material_family(name: str) -> str: """نام خانواده (بدون واریانت).""" family, _ = split_material_name(name) return " ".join(family.split()) def material_family_key(name: str) -> str: """شناسه‌ی پایدار خانواده — همان نام نرمال‌شده (بدون UUID اختصاصی).""" return material_family(name) def variant_label(name: str) -> str: """برچسب واریانت؛ در نبود واریانت، «استاندارد».""" _, variant = split_material_name(name) return variant or STANDARD_VARIANT_LABEL def spec_label_from_specs(specs: dict | None) -> str | None: """برچسب کوتاه مشخصات از JSON موجودی (فقط مقدارها، نه «کلید: مقدار»).""" if not specs: return None values = [str(value).strip() for value in specs.values() if str(value).strip()] return "، ".join(values) if values else None def material_entry_labels(material_name: str, specs: dict | None) -> dict: """برچسب‌های نمایشی یک ردیف کاتالوگ: خانواده، واریانت، مشخصه. `spec_label` (برچسب محور X) = مقدار مشخصات JSON اگر موجود باشد، وگرنه واریانتِ استخراج‌شده از نام، وگرنه «استاندارد». """ family = material_family(material_name) variant = variant_label(material_name) spec_from_json = spec_label_from_specs(specs) # برچسب محور X: واریانت (سایز/گرید) ترجیح دارد؛ در نبود آن مقدار مشخصات axis_label = variant if variant != STANDARD_VARIANT_LABEL else (spec_from_json or STANDARD_VARIANT_LABEL) return { "family_id": material_family_key(material_name), "family_name": family, "variant_label": variant, "spec_label": axis_label, "spec_detail": spec_from_json, }