uzkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uzkit/__init__.py +25 -0
- uzkit/data/regions.json +16 -0
- uzkit/formatters.py +1 -0
- uzkit/money.py +56 -0
- uzkit/numbers.py +126 -0
- uzkit/py.typed +0 -0
- uzkit/regions.py +42 -0
- uzkit/translit.py +105 -0
- uzkit/utils.py +1 -0
- uzkit/validators.py +112 -0
- uzkit-0.1.0.dist-info/METADATA +70 -0
- uzkit-0.1.0.dist-info/RECORD +14 -0
- uzkit-0.1.0.dist-info/WHEEL +4 -0
- uzkit-0.1.0.dist-info/licenses/LICENSE +21 -0
uzkit/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""uzkit - O'zbekiston uchun kichik yordamchi vositalar to'plami."""
|
|
2
|
+
|
|
3
|
+
from uzkit.money import format_money, money_to_words
|
|
4
|
+
from uzkit.numbers import number_to_words, ordinal
|
|
5
|
+
from uzkit.regions import Region, get_region, list_regions
|
|
6
|
+
from uzkit.translit import to_cyrillic, to_latin
|
|
7
|
+
from uzkit.validators import is_valid_phone, is_valid_plate, normalize_phone
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"Region",
|
|
14
|
+
"format_money",
|
|
15
|
+
"get_region",
|
|
16
|
+
"is_valid_phone",
|
|
17
|
+
"is_valid_plate",
|
|
18
|
+
"list_regions",
|
|
19
|
+
"money_to_words",
|
|
20
|
+
"normalize_phone",
|
|
21
|
+
"number_to_words",
|
|
22
|
+
"ordinal",
|
|
23
|
+
"to_cyrillic",
|
|
24
|
+
"to_latin",
|
|
25
|
+
]
|
uzkit/data/regions.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"name": "Andijon viloyati", "name_cyrillic": "Андижон вилояти", "capital": "Andijon", "capital_cyrillic": "Андижон", "plate_code": "60"},
|
|
3
|
+
{"name": "Buxoro viloyati", "name_cyrillic": "Бухоро вилояти", "capital": "Buxoro", "capital_cyrillic": "Бухоро", "plate_code": "80"},
|
|
4
|
+
{"name": "Farg'ona viloyati", "name_cyrillic": "Фарғона вилояти", "capital": "Farg'ona", "capital_cyrillic": "Фарғона", "plate_code": "40"},
|
|
5
|
+
{"name": "Jizzax viloyati", "name_cyrillic": "Жиззах вилояти", "capital": "Jizzax", "capital_cyrillic": "Жиззах", "plate_code": "25"},
|
|
6
|
+
{"name": "Namangan viloyati", "name_cyrillic": "Наманган вилояти", "capital": "Namangan", "capital_cyrillic": "Наманган", "plate_code": "50"},
|
|
7
|
+
{"name": "Navoiy viloyati", "name_cyrillic": "Навоий вилояти", "capital": "Navoiy", "capital_cyrillic": "Навоий", "plate_code": "85"},
|
|
8
|
+
{"name": "Qashqadaryo viloyati", "name_cyrillic": "Қашқадарё вилояти", "capital": "Qarshi", "capital_cyrillic": "Қарши", "plate_code": "70"},
|
|
9
|
+
{"name": "Qoraqalpog'iston Respublikasi", "name_cyrillic": "Қорақалпоғистон Республикаси", "capital": "Nukus", "capital_cyrillic": "Нукус", "plate_code": "95"},
|
|
10
|
+
{"name": "Samarqand viloyati", "name_cyrillic": "Самарқанд вилояти", "capital": "Samarqand", "capital_cyrillic": "Самарқанд", "plate_code": "30"},
|
|
11
|
+
{"name": "Sirdaryo viloyati", "name_cyrillic": "Сирдарё вилояти", "capital": "Guliston", "capital_cyrillic": "Гулистон", "plate_code": "20"},
|
|
12
|
+
{"name": "Surxondaryo viloyati", "name_cyrillic": "Сурхондарё вилояти", "capital": "Termiz", "capital_cyrillic": "Термиз", "plate_code": "75"},
|
|
13
|
+
{"name": "Toshkent shahri", "name_cyrillic": "Тошкент шаҳри", "capital": "Toshkent", "capital_cyrillic": "Тошкент", "plate_code": "01"},
|
|
14
|
+
{"name": "Toshkent viloyati", "name_cyrillic": "Тошкент вилояти", "capital": "Nurafshon", "capital_cyrillic": "Нурафшон", "plate_code": "10"},
|
|
15
|
+
{"name": "Xorazm viloyati", "name_cyrillic": "Хоразм вилояти", "capital": "Urganch", "capital_cyrillic": "Урганч", "plate_code": "90"}
|
|
16
|
+
]
|
uzkit/formatters.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Formatlash yordamchilari (hozircha bo'sh)."""
|
uzkit/money.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""So'm summalarini so'zga aylantirish va formatlash."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import ROUND_HALF_UP, Decimal
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from uzkit.numbers import number_to_words
|
|
9
|
+
from uzkit.translit import OKINA
|
|
10
|
+
|
|
11
|
+
SOM = f"so{OKINA}m"
|
|
12
|
+
TIYIN = "tiyin"
|
|
13
|
+
|
|
14
|
+
Amount = Union[int, float, Decimal, str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _to_decimal(amount: Amount) -> Decimal:
|
|
18
|
+
if isinstance(amount, bool) or not isinstance(amount, (int, float, Decimal, str)):
|
|
19
|
+
raise TypeError(f"Unsupported amount type: {type(amount).__name__}")
|
|
20
|
+
return Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def money_to_words(amount: Amount, *, with_tiyin: bool = True) -> str:
|
|
24
|
+
"""Summani so'm va tiyinda so'z bilan yozadi.
|
|
25
|
+
|
|
26
|
+
>>> money_to_words(1250)
|
|
27
|
+
'bir ming ikki yuz ellik soʻm'
|
|
28
|
+
>>> money_to_words("1250.50")
|
|
29
|
+
'bir ming ikki yuz ellik soʻm ellik tiyin'
|
|
30
|
+
"""
|
|
31
|
+
value = _to_decimal(amount)
|
|
32
|
+
sign = "minus " if value < 0 else ""
|
|
33
|
+
value = abs(value)
|
|
34
|
+
som = int(value)
|
|
35
|
+
tiyin = int((value - som) * 100)
|
|
36
|
+
parts = [number_to_words(som), SOM]
|
|
37
|
+
if with_tiyin and tiyin:
|
|
38
|
+
parts += [number_to_words(tiyin), TIYIN]
|
|
39
|
+
return sign + " ".join(parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_money(amount: Amount) -> str:
|
|
43
|
+
"""Summani o'qishga qulay ko'rinishda yozadi.
|
|
44
|
+
|
|
45
|
+
>>> format_money(1250000)
|
|
46
|
+
'1 250 000 soʻm'
|
|
47
|
+
"""
|
|
48
|
+
value = _to_decimal(amount)
|
|
49
|
+
sign = "-" if value < 0 else ""
|
|
50
|
+
value = abs(value)
|
|
51
|
+
som = int(value)
|
|
52
|
+
tiyin = int((value - som) * 100)
|
|
53
|
+
text = f"{som:,}".replace(",", " ")
|
|
54
|
+
if tiyin:
|
|
55
|
+
text += f",{tiyin:02d}"
|
|
56
|
+
return f"{sign}{text} {SOM}"
|
uzkit/numbers.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Sonlarni o'zbekcha so'zlar bilan yozish (0 dan 10**12 gacha)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from uzkit.translit import OKINA
|
|
8
|
+
|
|
9
|
+
__all__ = ["MAX_NUMBER", "number_to_words", "ordinal"]
|
|
10
|
+
|
|
11
|
+
#: Qo'llab-quvvatlanadigan eng katta son (modul bo'yicha).
|
|
12
|
+
MAX_NUMBER = 10**12
|
|
13
|
+
|
|
14
|
+
_ONES = (
|
|
15
|
+
"",
|
|
16
|
+
"bir",
|
|
17
|
+
"ikki",
|
|
18
|
+
"uch",
|
|
19
|
+
f"to{OKINA}rt",
|
|
20
|
+
"besh",
|
|
21
|
+
"olti",
|
|
22
|
+
"yetti",
|
|
23
|
+
"sakkiz",
|
|
24
|
+
f"to{OKINA}qqiz",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_TENS = (
|
|
28
|
+
"",
|
|
29
|
+
f"o{OKINA}n",
|
|
30
|
+
"yigirma",
|
|
31
|
+
f"o{OKINA}ttiz",
|
|
32
|
+
"qirq",
|
|
33
|
+
"ellik",
|
|
34
|
+
"oltmish",
|
|
35
|
+
"yetmish",
|
|
36
|
+
"sakson",
|
|
37
|
+
f"to{OKINA}qson",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_SCALES = (
|
|
41
|
+
(10**12, "trillion"),
|
|
42
|
+
(10**9, "milliard"),
|
|
43
|
+
(10**6, "million"),
|
|
44
|
+
(10**3, "ming"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_VOWELS = frozenset("aeiou")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate(n: object) -> None:
|
|
51
|
+
if isinstance(n, bool) or not isinstance(n, int):
|
|
52
|
+
raise TypeError(f"n must be int, not {type(n).__name__}")
|
|
53
|
+
if abs(n) > MAX_NUMBER:
|
|
54
|
+
raise ValueError(f"n must be within [-{MAX_NUMBER}, {MAX_NUMBER}], got {n}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _below_thousand(n: int) -> List[str]:
|
|
58
|
+
"""1..999 oralig'idagi sonni so'zlar ro'yxati sifatida qaytaradi."""
|
|
59
|
+
words: List[str] = []
|
|
60
|
+
hundreds, rest = divmod(n, 100)
|
|
61
|
+
if hundreds:
|
|
62
|
+
words += [_ONES[hundreds], "yuz"]
|
|
63
|
+
tens, ones = divmod(rest, 10)
|
|
64
|
+
if tens:
|
|
65
|
+
words.append(_TENS[tens])
|
|
66
|
+
if ones:
|
|
67
|
+
words.append(_ONES[ones])
|
|
68
|
+
return words
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def number_to_words(n: int) -> str:
|
|
72
|
+
"""Butun sonni o'zbekcha so'zlar bilan yozadi.
|
|
73
|
+
|
|
74
|
+
>>> number_to_words(1250)
|
|
75
|
+
'bir ming ikki yuz ellik'
|
|
76
|
+
>>> number_to_words(0)
|
|
77
|
+
'nol'
|
|
78
|
+
>>> number_to_words(-5)
|
|
79
|
+
'minus besh'
|
|
80
|
+
|
|
81
|
+
Oraliq: ``-10**12 <= n <= 10**12``. Manfiy sonlar ``minus`` bilan
|
|
82
|
+
boshlanadi. ``yuz`` va ``ming`` oldidan ``bir`` doim yoziladi
|
|
83
|
+
(``100`` -> ``bir yuz``).
|
|
84
|
+
|
|
85
|
+
``oʻ`` va ``gʻ`` dagi belgi :data:`uzkit.translit.OKINA` (U+02BB).
|
|
86
|
+
|
|
87
|
+
``int`` bo'lmagan (``bool`` ham) kiritish uchun ``TypeError``, oraliqdan
|
|
88
|
+
tashqari son uchun ``ValueError`` ko'taradi.
|
|
89
|
+
"""
|
|
90
|
+
_validate(n)
|
|
91
|
+
if n == 0:
|
|
92
|
+
return "nol"
|
|
93
|
+
|
|
94
|
+
words: List[str] = ["minus"] if n < 0 else []
|
|
95
|
+
remainder = abs(n)
|
|
96
|
+
for value, name in _SCALES:
|
|
97
|
+
count, remainder = divmod(remainder, value)
|
|
98
|
+
if count:
|
|
99
|
+
words += _below_thousand(count) + [name]
|
|
100
|
+
words += _below_thousand(remainder)
|
|
101
|
+
return " ".join(words)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def ordinal(n: int) -> str:
|
|
105
|
+
"""Tartib sonni yozadi: oxirgi so'zga ``-inchi`` yoki ``-nchi`` qo'shiladi.
|
|
106
|
+
|
|
107
|
+
>>> ordinal(3)
|
|
108
|
+
'uchinchi'
|
|
109
|
+
>>> ordinal(2)
|
|
110
|
+
'ikkinchi'
|
|
111
|
+
>>> ordinal(1250)
|
|
112
|
+
'bir ming ikki yuz ellikinchi'
|
|
113
|
+
|
|
114
|
+
Oxirgi so'z unli bilan tugasa ``-nchi``, undosh bilan tugasa ``-inchi``
|
|
115
|
+
qo'shiladi. Oraliq: ``0 <= n <= 10**12``.
|
|
116
|
+
|
|
117
|
+
``int`` bo'lmagan kiritish uchun ``TypeError``, manfiy yoki oraliqdan
|
|
118
|
+
tashqari son uchun ``ValueError`` ko'taradi.
|
|
119
|
+
"""
|
|
120
|
+
_validate(n)
|
|
121
|
+
if n < 0:
|
|
122
|
+
raise ValueError(f"n must be non-negative, got {n}")
|
|
123
|
+
|
|
124
|
+
words = number_to_words(n)
|
|
125
|
+
suffix = "nchi" if words[-1] in _VOWELS else "inchi"
|
|
126
|
+
return words + suffix
|
uzkit/py.typed
ADDED
|
File without changes
|
uzkit/regions.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""O'zbekiston viloyatlari va tarkibiy birliklari."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
__all__ = ["Region", "get_region", "list_regions"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Region:
|
|
16
|
+
name: str
|
|
17
|
+
name_cyrillic: str
|
|
18
|
+
capital: str
|
|
19
|
+
capital_cyrillic: str
|
|
20
|
+
plate_code: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache(maxsize=1)
|
|
24
|
+
def _regions() -> tuple[Region, ...]:
|
|
25
|
+
path = Path(__file__).resolve().parent / "data" / "regions.json"
|
|
26
|
+
with path.open(encoding="utf-8") as file:
|
|
27
|
+
raw = json.load(file)
|
|
28
|
+
return tuple(Region(**item) for item in raw)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_regions() -> list[Region]:
|
|
32
|
+
return list(_regions())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_region(name: str) -> Optional[Region]:
|
|
36
|
+
if not isinstance(name, str) or not name.strip():
|
|
37
|
+
return None
|
|
38
|
+
needle = name.casefold()
|
|
39
|
+
for region in _regions():
|
|
40
|
+
if region.name.casefold() == needle or region.name_cyrillic.casefold() == needle:
|
|
41
|
+
return region
|
|
42
|
+
return None
|
uzkit/translit.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""O'zbek tili uchun Lotin <-> Kirill transliteratsiyasi."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
OKINA = "\u02bb" # ʻ o‘ va g‘ dagi belgi
|
|
6
|
+
TUTUQ = "\u02bc" # ʼ tutuq belgisi (ma’no)
|
|
7
|
+
|
|
8
|
+
_APOSTROPHES = frozenset("'\u2019\u02bb\u2018`\u00b4\u02bc")
|
|
9
|
+
_LAT_VOWELS = frozenset("aeiou")
|
|
10
|
+
_CYR_VOWELS = frozenset("аеёиоуўэюяы")
|
|
11
|
+
|
|
12
|
+
_LAT2CYR = {
|
|
13
|
+
"a": "а", "b": "б", "d": "д", "f": "ф", "g": "г", "h": "ҳ",
|
|
14
|
+
"i": "и", "j": "ж", "k": "к", "l": "л", "m": "м", "n": "н",
|
|
15
|
+
"o": "о", "p": "п", "q": "қ", "r": "р", "s": "с", "t": "т",
|
|
16
|
+
"u": "у", "v": "в", "x": "х", "y": "й", "z": "з",
|
|
17
|
+
}
|
|
18
|
+
_Y_COMBOS = {"o": "ё", "u": "ю", "a": "я", "e": "е"}
|
|
19
|
+
|
|
20
|
+
_CYR2LAT = {
|
|
21
|
+
"а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "ё": "yo",
|
|
22
|
+
"ж": "j", "з": "z", "и": "i", "й": "y", "к": "k", "л": "l",
|
|
23
|
+
"м": "m", "н": "n", "о": "o", "п": "p", "р": "r", "с": "s",
|
|
24
|
+
"т": "t", "у": "u", "ф": "f", "х": "x", "ц": "ts", "ч": "ch",
|
|
25
|
+
"ш": "sh", "щ": "shch", "ъ": TUTUQ, "ы": "i", "ь": "", "э": "e",
|
|
26
|
+
"ю": "yu", "я": "ya", "ў": "o" + OKINA, "қ": "q",
|
|
27
|
+
"ғ": "g" + OKINA, "ҳ": "h",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_cyrillic(text: str) -> str:
|
|
32
|
+
"""Lotin yozuvidagi matnni Kirillga o'giradi.
|
|
33
|
+
|
|
34
|
+
>>> to_cyrillic("O'zbekiston")
|
|
35
|
+
'Ўзбекистон'
|
|
36
|
+
"""
|
|
37
|
+
out: list[str] = []
|
|
38
|
+
n = len(text)
|
|
39
|
+
i = 0
|
|
40
|
+
while i < n:
|
|
41
|
+
ch = text[i]
|
|
42
|
+
low = ch.lower()
|
|
43
|
+
nxt = text[i + 1] if i + 1 < n else ""
|
|
44
|
+
nlow = nxt.lower()
|
|
45
|
+
prev = text[i - 1] if i > 0 else ""
|
|
46
|
+
step = 1
|
|
47
|
+
|
|
48
|
+
if low in ("o", "g") and nxt in _APOSTROPHES:
|
|
49
|
+
res = "ў" if low == "o" else "ғ"
|
|
50
|
+
step = 2
|
|
51
|
+
elif low in ("s", "c") and nlow == "h":
|
|
52
|
+
res = "ш" if low == "s" else "ч"
|
|
53
|
+
step = 2
|
|
54
|
+
elif low == "y" and nlow in _Y_COMBOS:
|
|
55
|
+
res = _Y_COMBOS[nlow]
|
|
56
|
+
step = 2
|
|
57
|
+
elif low == "e":
|
|
58
|
+
at_start = not prev.isalpha() or prev.lower() in _LAT_VOWELS
|
|
59
|
+
res = "э" if at_start else "е"
|
|
60
|
+
elif ch in _APOSTROPHES and prev.isalpha() and nxt.isalpha():
|
|
61
|
+
res = "ъ"
|
|
62
|
+
elif low in _LAT2CYR:
|
|
63
|
+
res = _LAT2CYR[low]
|
|
64
|
+
else:
|
|
65
|
+
out.append(ch)
|
|
66
|
+
i += 1
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
out.append(res.upper() if ch.isupper() else res)
|
|
70
|
+
i += step
|
|
71
|
+
return "".join(out)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _has_upper_neighbor(text: str, i: int) -> bool:
|
|
75
|
+
before = i > 0 and text[i - 1].isupper()
|
|
76
|
+
after = i + 1 < len(text) and text[i + 1].isupper()
|
|
77
|
+
return before or after
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_latin(text: str) -> str:
|
|
81
|
+
"""Kirill yozuvidagi matnni Lotinga o'giradi (2023 alifbosi).
|
|
82
|
+
|
|
83
|
+
>>> to_latin("Ўзбекистон")
|
|
84
|
+
'Oʻzbekiston'
|
|
85
|
+
"""
|
|
86
|
+
out: list[str] = []
|
|
87
|
+
for i, ch in enumerate(text):
|
|
88
|
+
low = ch.lower()
|
|
89
|
+
if low == "е":
|
|
90
|
+
prev = text[i - 1].lower() if i > 0 else ""
|
|
91
|
+
ye = not prev.isalpha() or prev in _CYR_VOWELS or prev in "ъь"
|
|
92
|
+
res = "ye" if ye else "e"
|
|
93
|
+
elif low in _CYR2LAT:
|
|
94
|
+
res = _CYR2LAT[low]
|
|
95
|
+
else:
|
|
96
|
+
out.append(ch)
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if ch.isupper() and res:
|
|
100
|
+
if len(res) > 1 and not _has_upper_neighbor(text, i):
|
|
101
|
+
res = res.capitalize()
|
|
102
|
+
else:
|
|
103
|
+
res = res.upper()
|
|
104
|
+
out.append(res)
|
|
105
|
+
return "".join(out)
|
uzkit/utils.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Umumiy yordamchi funksiyalar (hozircha bo'sh)."""
|
uzkit/validators.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""O'zbekiston telefon raqamlari va avtomobil davlat raqamlarini tekshirish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MOBILE_OPERATOR_CODES",
|
|
10
|
+
"is_valid_phone",
|
|
11
|
+
"is_valid_plate",
|
|
12
|
+
"normalize_phone",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
COUNTRY_CODE = "998"
|
|
16
|
+
|
|
17
|
+
#: Mobil operator kodlari (+998 XX ...).
|
|
18
|
+
MOBILE_OPERATOR_CODES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"20", # Sardor Telecom / OQ
|
|
21
|
+
"33", # Humans
|
|
22
|
+
"50", # Ucell
|
|
23
|
+
"55", # Ucell; VoIP (Uztelecom), mobil emas
|
|
24
|
+
"77", # Uzmobile
|
|
25
|
+
"88", # Mobiuz / Humans
|
|
26
|
+
"90", # Beeline
|
|
27
|
+
"91", # Beeline
|
|
28
|
+
"93", # Ucell
|
|
29
|
+
"94", # Ucell
|
|
30
|
+
"95", # Uzmobile
|
|
31
|
+
"97", # Mobiuz
|
|
32
|
+
"98", # Mobiuz
|
|
33
|
+
"99", # Uzmobile
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Raqamlar orasida ruxsat etilgan ajratuvchilar: bo'shliq, tire, nuqta, qavs.
|
|
38
|
+
_PHONE_SEPARATORS = re.compile(r"[\s\-.()]+")
|
|
39
|
+
_PHONE_BODY = re.compile(r"\+?[0-9]+", re.ASCII)
|
|
40
|
+
|
|
41
|
+
# 01 A 123 AA (jismoniy shaxs) va 01 123 AAA (yuridik shaxs)
|
|
42
|
+
_PLATE_PERSONAL = re.compile(r"(\d{2}) ?([A-Z]) ?(\d{3}) ?([A-Z]{2})", re.ASCII)
|
|
43
|
+
_PLATE_LEGAL = re.compile(r"(\d{2}) ?(\d{3}) ?([A-Z]{3})", re.ASCII)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_phone(phone: Any) -> Optional[str]:
|
|
47
|
+
"""Telefon raqamini ``+998XXXXXXXXX`` ko'rinishiga keltiradi.
|
|
48
|
+
|
|
49
|
+
Qabul qilinadigan kiritishlar (bo'shliq, tire, nuqta va qavslar e'tiborga
|
|
50
|
+
olinmaydi)::
|
|
51
|
+
|
|
52
|
+
+998 90 123-45-67
|
|
53
|
+
998901234567
|
|
54
|
+
90 123 45 67 (milliy 9 xonali format)
|
|
55
|
+
|
|
56
|
+
Raqam noto'g'ri bo'lsa (yoki ``str`` bo'lmasa) ``None`` qaytariladi.
|
|
57
|
+
Faqat mobil operator kodlari (:data:`MOBILE_OPERATOR_CODES`) qabul qilinadi.
|
|
58
|
+
"""
|
|
59
|
+
if not isinstance(phone, str):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
cleaned = _PHONE_SEPARATORS.sub("", phone)
|
|
63
|
+
if not _PHONE_BODY.fullmatch(cleaned):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
digits = cleaned[1:] if cleaned.startswith("+") else cleaned
|
|
67
|
+
|
|
68
|
+
if len(digits) == 12 and digits.startswith(COUNTRY_CODE):
|
|
69
|
+
national = digits[len(COUNTRY_CODE):]
|
|
70
|
+
elif len(digits) == 9 and not cleaned.startswith("+"):
|
|
71
|
+
national = digits
|
|
72
|
+
else:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if national[:2] not in MOBILE_OPERATOR_CODES:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return f"+{COUNTRY_CODE}{national}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_valid_phone(phone: Any) -> bool:
|
|
82
|
+
"""Telefon raqami O'zbekiston mobil raqami ekanini tekshiradi.
|
|
83
|
+
|
|
84
|
+
>>> is_valid_phone("+998 90 123 45 67")
|
|
85
|
+
True
|
|
86
|
+
>>> is_valid_phone("+998 10 123 45 67")
|
|
87
|
+
False
|
|
88
|
+
"""
|
|
89
|
+
return normalize_phone(phone) is not None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_valid_plate(plate: Any) -> bool:
|
|
93
|
+
"""Avtomobil davlat raqamini tekshiradi.
|
|
94
|
+
|
|
95
|
+
Qo'llab-quvvatlanadigan formatlar::
|
|
96
|
+
|
|
97
|
+
01 A 123 AA (jismoniy shaxslar)
|
|
98
|
+
01 123 AAA (yuridik shaxslar)
|
|
99
|
+
|
|
100
|
+
Bo'shliqlar ixtiyoriy (``01A123AA``), harflar katta-kichikligi farqlanmaydi.
|
|
101
|
+
Viloyat kodi 00 dan farqli ikki xonali kod bo'lishi shart (01-99).
|
|
102
|
+
"""
|
|
103
|
+
if not isinstance(plate, str):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
value = plate.strip().upper()
|
|
107
|
+
match = _PLATE_PERSONAL.fullmatch(value) or _PLATE_LEGAL.fullmatch(value)
|
|
108
|
+
if match is None:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
code = match.group(1)
|
|
112
|
+
return code != "00" and code.isdigit() and len(code) == 2
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: uzkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: O'zbek tili va ma'lumotlari uchun Python kutubxonasi: transliteratsiya, raqamlarni so'zga aylantirish, so'm summalari, telefon va avtoraqam validatorlari, viloyatlar.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mr-Ali-web/Uzkit
|
|
6
|
+
Project-URL: Issues, https://github.com/Mr-Ali-web/Uzkit/issues
|
|
7
|
+
Author: Ali
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: license-plate,numbers,phone,transliteration,uzbek,uzbekistan,validators
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
[](https://github.com/Mr-Ali-web/uzkit/actions/workflows/tests.yml)
|
|
29
|
+
|
|
30
|
+
# uzkit
|
|
31
|
+
|
|
32
|
+
O'zbek tili va ma'lumotlari uchun yordamchi Python kutubxonasi.
|
|
33
|
+
Small Python library for Uzbek language and data helpers.
|
|
34
|
+
|
|
35
|
+
## O'rnatish / Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e ".[dev]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Misollar / Examples
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from uzkit import to_cyrillic, to_latin, number_to_words, money_to_words, format_money, normalize_phone, is_valid_phone, is_valid_plate, ordinal
|
|
45
|
+
|
|
46
|
+
to_cyrillic("Salom, dunyo!") # 'Салом, дунё!'
|
|
47
|
+
to_latin("Салом, дунё!") # 'Salom, dunyo!'
|
|
48
|
+
number_to_words(1250) # 'bir ming ikki yuz ellik'
|
|
49
|
+
money_to_words(1250) # 'bir ming ikki yuz ellik soʻm'
|
|
50
|
+
format_money(1250000) # '1 250 000 soʻm'
|
|
51
|
+
normalize_phone("+998 90 123 45 67") # '+998901234567'
|
|
52
|
+
is_valid_phone("+998 90 123 45 67") # True
|
|
53
|
+
is_valid_plate("01A123BC") # True
|
|
54
|
+
ordinal(3) # 'uchinchi'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Testlar / Tests
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pytest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Cheklovlar / Limitations
|
|
64
|
+
|
|
65
|
+
- Telefon kodlari Wikipedia va operator sahifalariga asoslangan, regulyatorning rasmiy hujjati bilan tekshirilmagan.
|
|
66
|
+
- Faqat mobil raqamlar qo'llab-quvvatlanadi.
|
|
67
|
+
- Viloyatlar ro'yxati Wikipedia/ISO 3166-2:UZ manbalariga asoslangan; rasmiy davlat manbasi bilan to'liq solishtirilmagan.
|
|
68
|
+
|
|
69
|
+
Operator and region codes are not verified against an official source. Only mobile phone numbers are supported.
|
|
70
|
+
Region list is based on Wikipedia/ISO 3166-2:UZ and has not been fully reconciled with official government sources.# Uzkit
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
uzkit/__init__.py,sha256=pISo_MTs9z7SgmIibuNWjsawsHlPfuTK9RC3x4saoxQ,650
|
|
2
|
+
uzkit/formatters.py,sha256=sbg1hZ4UPdAPo2-lRKvtaHZYGbVZFG7q9gRrCaAAD5g,49
|
|
3
|
+
uzkit/money.py,sha256=8K-ZHunER85jT_flq5s1Crtvna6v5vTB3GqqweJ58wU,1615
|
|
4
|
+
uzkit/numbers.py,sha256=b6kIKglSqwbBuivSvRx7feshP6UH-TBOaxTtefucZOw,3061
|
|
5
|
+
uzkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
uzkit/regions.py,sha256=s0alAZSB-4fbp20Sf9h6-T9xwzU7sV63Ov_9OvYZOpw,1050
|
|
7
|
+
uzkit/translit.py,sha256=k_UClZQ5zVC8bT5Dy0R9wHRWv1QybS7JkkG3NaWeh8s,3366
|
|
8
|
+
uzkit/utils.py,sha256=pYn4mzXJIHHd6mOfRME7FecOyRrZj7p5BHEsyl6lg9s,53
|
|
9
|
+
uzkit/validators.py,sha256=ANt4gA_yOJg17sQjewWKa5ysn5YDNwUUCTjq2CGy2YY,3157
|
|
10
|
+
uzkit/data/regions.json,sha256=vA3LFkKK_jNtHfImo-IgIXcXuyZKLMAtIlElmXjDPkY,2316
|
|
11
|
+
uzkit-0.1.0.dist-info/METADATA,sha256=Gkxwzsy-0G6Bg7RepB6zOAoIUS5y-cf7gPsVNoRnZKc,2801
|
|
12
|
+
uzkit-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
13
|
+
uzkit-0.1.0.dist-info/licenses/LICENSE,sha256=Bz5H3MtpNpTVPvu3BCs2Ecaafpc1NSQ-vekroqlZc9I,1060
|
|
14
|
+
uzkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|