91 lines
3.9 KiB
Python
91 lines
3.9 KiB
Python
"""محاسبهٔ «تعداد پیامکِ محاسبهشده» (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),
|
||
}
|