"""محاسبهٔ «تعداد پیامکِ محاسبه‌شده» (parts) و کدگذاری متن. هر پیامک روی شبکهٔ موبایل بر اساس **کدگذاری** و **طول** به یک یا چند «بخش» (part) شکسته می‌شود و اپراتور بر همان تعداد بخش هزینه می‌گیرد. این ماژول همان قاعدهٔ استاندارد (3GPP/GSM 03.38) را پیاده می‌کند تا بتوان هزینهٔ ارسال را تحلیل کرد: * **GSM-7** — برای متن‌های انگلیسی/عددی. هر بخش ۱۶۰ کاراکتر؛ در حالت چندبخشی (multipart) هر بخش ۱۵۳ کاراکتر (۷ کاراکتر برای هدر الحاق). برخی کاراکترها (مثل `{}[]~\\|^€`) در جدول «الحاقی» هستند و **۲ کاراکتر** حساب می‌شوند. * **UCS-2** — اگر متن حتی **یک** کاراکتر خارج از جدول GSM-7 داشته باشد (فارسی، عربی، ایموجی…)، **کل پیامک** با UCS-2 کدگذاری می‌شود: هر بخش ۷۰ کاراکتر؛ در حالت چندبخشی هر بخش ۶۷ کاراکتر. ایموجی‌ها (خارج از BMP) دو واحد کدِ UTF-16 حساب می‌شوند. بنابراین یک متن فارسی حتی اگر کوتاه باشد، در «۷۰ کاراکتر» جا می‌شود و ممکن است چند پیامک حساب شود؛ و یک متن ترکیبی فارسی+انگلیسی همیشه UCS-2 است. """ from math import ceil ENCODING_GSM7 = "gsm7" ENCODING_UCS2 = "ucs2" # جدول کامل GSM 03.38 (بخش پایه). کاراکتر 0x1B عمداً وارد نشده چون نقش «پیشوند # الحاق» دارد و کاراکتر مستقل قانونی نیست. GSM7_BASIC = set( "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ" " !\"#¤%&'()*+,-./0123456789:;<=>?" "¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ" "¿abcdefghijklmnopqrstuvwxyzäöñüà" ) # کاراکترهای «جدول الحاقی»: در GSM-7 دو واحد حساب می‌شوند. GSM7_EXTENDED = set("^{}\\[~]|€") # (سقف تک‌بخشی، سقف هر بخش چندبخشی) برای هر کدگذاری. LIMITS = { ENCODING_GSM7: (160, 153), ENCODING_UCS2: (70, 67), } ENCODING_LABELS = { ENCODING_GSM7: "GSM-7 (انگلیسی/عدد)", ENCODING_UCS2: "UCS-2 (فارسی/یونیکد)", } def is_gsm7(text): """آیا کل متن با جدول GSM-7 قابل نمایش است؟ (شرط کافی و لازم برای GSM-7).""" return all(ch in GSM7_BASIC or ch in GSM7_EXTENDED for ch in text or "") def detect_encoding(text): """کدگذاری متن: ``gsm7`` یا ``ucs2``. متن خالی = ``gsm7``.""" return ENCODING_GSM7 if is_gsm7(text) else ENCODING_UCS2 def encoded_length(text): """طول متن بر حسب «واحدهایی که اپراتور می‌شمارد». * GSM-7: کاراکترهای الحاقی ۲ واحد. * UCS-2: واحدهای کد UTF-16 (ایموجی = ۲). """ text = text or "" if is_gsm7(text): return sum(2 if ch in GSM7_EXTENDED else 1 for ch in text) return len(text.encode("utf-16-le")) // 2 def parts_count(text): """تعداد بخش‌هایی که اپراتور برای این متن حساب می‌کند (۱ برای متن خالی = ۰).""" text = text or "" if not text: return 0 single, multipart = LIMITS[detect_encoding(text)] length = encoded_length(text) if length <= single: return 1 return ceil(length / multipart) def describe(text): """خلاصهٔ محاسبه برای نمایش در گزارش (کدگذاری، طول، تعداد بخش).""" encoding = detect_encoding(text) return { "encoding": encoding, "encoding_label": ENCODING_LABELS[encoding], "length": encoded_length(text), "parts": parts_count(text), }