Files
2026-10-11 19:04:20 +03:30

91 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""محاسبهٔ «تعداد پیامکِ محاسبه‌شده» (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),
}