uzkit 0.1.0__tar.gz
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-0.1.0/.github/workflows/tests.yml +21 -0
- uzkit-0.1.0/.gitignore +8 -0
- uzkit-0.1.0/.vscode/settings.json +3 -0
- uzkit-0.1.0/LICENSE +21 -0
- uzkit-0.1.0/PKG-INFO +70 -0
- uzkit-0.1.0/README.md +43 -0
- uzkit-0.1.0/pyproject.toml +43 -0
- uzkit-0.1.0/src/uzkit/__init__.py +25 -0
- uzkit-0.1.0/src/uzkit/data/regions.json +16 -0
- uzkit-0.1.0/src/uzkit/formatters.py +1 -0
- uzkit-0.1.0/src/uzkit/money.py +56 -0
- uzkit-0.1.0/src/uzkit/numbers.py +126 -0
- uzkit-0.1.0/src/uzkit/py.typed +0 -0
- uzkit-0.1.0/src/uzkit/regions.py +42 -0
- uzkit-0.1.0/src/uzkit/translit.py +105 -0
- uzkit-0.1.0/src/uzkit/utils.py +1 -0
- uzkit-0.1.0/src/uzkit/validators.py +112 -0
- uzkit-0.1.0/tests/test_money.py +45 -0
- uzkit-0.1.0/tests/test_numbers.py +61 -0
- uzkit-0.1.0/tests/test_regions.py +91 -0
- uzkit-0.1.0/tests/test_translit.py +43 -0
- uzkit-0.1.0/tests/test_validators.py +154 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pytest:
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: pytest
|
uzkit-0.1.0/.gitignore
ADDED
uzkit-0.1.0/LICENSE
ADDED
|
@@ -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.
|
uzkit-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
uzkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[](https://github.com/Mr-Ali-web/uzkit/actions/workflows/tests.yml)
|
|
2
|
+
|
|
3
|
+
# uzkit
|
|
4
|
+
|
|
5
|
+
O'zbek tili va ma'lumotlari uchun yordamchi Python kutubxonasi.
|
|
6
|
+
Small Python library for Uzbek language and data helpers.
|
|
7
|
+
|
|
8
|
+
## O'rnatish / Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install -e ".[dev]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Misollar / Examples
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from uzkit import to_cyrillic, to_latin, number_to_words, money_to_words, format_money, normalize_phone, is_valid_phone, is_valid_plate, ordinal
|
|
18
|
+
|
|
19
|
+
to_cyrillic("Salom, dunyo!") # 'Салом, дунё!'
|
|
20
|
+
to_latin("Салом, дунё!") # 'Salom, dunyo!'
|
|
21
|
+
number_to_words(1250) # 'bir ming ikki yuz ellik'
|
|
22
|
+
money_to_words(1250) # 'bir ming ikki yuz ellik soʻm'
|
|
23
|
+
format_money(1250000) # '1 250 000 soʻm'
|
|
24
|
+
normalize_phone("+998 90 123 45 67") # '+998901234567'
|
|
25
|
+
is_valid_phone("+998 90 123 45 67") # True
|
|
26
|
+
is_valid_plate("01A123BC") # True
|
|
27
|
+
ordinal(3) # 'uchinchi'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Testlar / Tests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pytest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Cheklovlar / Limitations
|
|
37
|
+
|
|
38
|
+
- Telefon kodlari Wikipedia va operator sahifalariga asoslangan, regulyatorning rasmiy hujjati bilan tekshirilmagan.
|
|
39
|
+
- Faqat mobil raqamlar qo'llab-quvvatlanadi.
|
|
40
|
+
- Viloyatlar ro'yxati Wikipedia/ISO 3166-2:UZ manbalariga asoslangan; rasmiy davlat manbasi bilan to'liq solishtirilmagan.
|
|
41
|
+
|
|
42
|
+
Operator and region codes are not verified against an official source. Only mobile phone numbers are supported.
|
|
43
|
+
Region list is based on Wikipedia/ISO 3166-2:UZ and has not been fully reconciled with official government sources.# Uzkit
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uzkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "O'zbek tili va ma'lumotlari uchun Python kutubxonasi: transliteratsiya, raqamlarni so'zga aylantirish, so'm summalari, telefon va avtoraqam validatorlari, viloyatlar."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Ali" }]
|
|
13
|
+
keywords = ["uzbekistan", "uzbek", "transliteration", "numbers", "validators", "phone", "license-plate"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Mr-Ali-web/Uzkit"
|
|
32
|
+
Issues = "https://github.com/Mr-Ali-web/Uzkit/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7.0"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/uzkit"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
pythonpath = ["src"]
|
|
43
|
+
addopts = "-ra"
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Formatlash yordamchilari (hozircha bo'sh)."""
|
|
@@ -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}"
|
|
@@ -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
|
|
File without changes
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Umumiy yordamchi funksiyalar (hozircha bo'sh)."""
|
|
@@ -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,45 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from uzkit import format_money, money_to_words
|
|
6
|
+
from uzkit.translit import OKINA
|
|
7
|
+
|
|
8
|
+
SOM = f"so{OKINA}m"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("amount,expected", [
|
|
12
|
+
(0, f"nol {SOM}"),
|
|
13
|
+
(1250, f"bir ming ikki yuz ellik {SOM}"),
|
|
14
|
+
("1250.50", f"bir ming ikki yuz ellik {SOM} ellik tiyin"),
|
|
15
|
+
(Decimal("0.05"), f"nol {SOM} besh tiyin"),
|
|
16
|
+
(-100, f"minus bir yuz {SOM}"),
|
|
17
|
+
(1_000_000, f"bir million {SOM}"),
|
|
18
|
+
])
|
|
19
|
+
def test_money_to_words(amount, expected):
|
|
20
|
+
assert money_to_words(amount) == expected
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_without_tiyin():
|
|
24
|
+
assert money_to_words(10.75, with_tiyin=False) == f"o{OKINA}n {SOM}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_float_precision():
|
|
28
|
+
assert money_to_words(0.1 + 0.2) == f"nol {SOM} o{OKINA}ttiz tiyin"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("bad", [None, True, [1], {}])
|
|
32
|
+
def test_bad_type(bad):
|
|
33
|
+
with pytest.raises(TypeError):
|
|
34
|
+
money_to_words(bad)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize("amount,expected", [
|
|
38
|
+
(1250000, f"1 250 000 {SOM}"),
|
|
39
|
+
(999, f"999 {SOM}"),
|
|
40
|
+
("1500.5", f"1 500,50 {SOM}"),
|
|
41
|
+
(-2000, f"-2 000 {SOM}"),
|
|
42
|
+
])
|
|
43
|
+
def test_format_money(amount, expected):
|
|
44
|
+
assert format_money(amount) == expected
|
|
45
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from uzkit import number_to_words, ordinal
|
|
4
|
+
from uzkit.translit import OKINA
|
|
5
|
+
|
|
6
|
+
O = OKINA
|
|
7
|
+
|
|
8
|
+
CARDINALS = [
|
|
9
|
+
(0, "nol"),
|
|
10
|
+
(1, "bir"),
|
|
11
|
+
(7, "yetti"),
|
|
12
|
+
(10, f"o{O}n"),
|
|
13
|
+
(14, f"o{O}n to{O}rt"),
|
|
14
|
+
(20, "yigirma"),
|
|
15
|
+
(40, "qirq"),
|
|
16
|
+
(80, "sakson"),
|
|
17
|
+
(90, f"to{O}qson"),
|
|
18
|
+
(100, "bir yuz"),
|
|
19
|
+
(101, "bir yuz bir"),
|
|
20
|
+
(1000, "bir ming"),
|
|
21
|
+
(1250, "bir ming ikki yuz ellik"),
|
|
22
|
+
(2024, f"ikki ming yigirma to{O}rt"),
|
|
23
|
+
(1_000_000, "bir million"),
|
|
24
|
+
(1_000_001, "bir million bir"),
|
|
25
|
+
(1_000_000_000, "bir milliard"),
|
|
26
|
+
(
|
|
27
|
+
12_345_678,
|
|
28
|
+
f"o{O}n ikki million uch yuz qirq besh ming olti yuz yetmish sakkiz",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("n,words", CARDINALS)
|
|
34
|
+
def test_number_to_words(n, words):
|
|
35
|
+
assert number_to_words(n) == words
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("n", [1, 5, 99, 1000, 123456])
|
|
39
|
+
def test_negative(n):
|
|
40
|
+
assert number_to_words(-n) == "minus " + number_to_words(n)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ORDINALS = [
|
|
44
|
+
(1, "birinchi"),
|
|
45
|
+
(2, "ikkinchi"),
|
|
46
|
+
(3, "uchinchi"),
|
|
47
|
+
(4, f"to{O}rtinchi"),
|
|
48
|
+
(5, "beshinchi"),
|
|
49
|
+
(6, "oltinchi"),
|
|
50
|
+
(7, "yettinchi"),
|
|
51
|
+
(8, "sakkizinchi"),
|
|
52
|
+
(9, f"to{O}qqizinchi"),
|
|
53
|
+
(10, f"o{O}ninchi"),
|
|
54
|
+
(20, "yigirmanchi"),
|
|
55
|
+
(40, "qirqinchi"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("n,words", ORDINALS)
|
|
60
|
+
def test_ordinal(n, words):
|
|
61
|
+
assert ordinal(n) == words
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from uzkit.regions import Region, get_region, list_regions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_list_regions_count():
|
|
7
|
+
assert len(list_regions()) == 14
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_list_regions_returns_region_objects():
|
|
11
|
+
assert all(isinstance(region, Region) for region in list_regions())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_get_region_by_latin_name():
|
|
15
|
+
region = get_region("Toshkent viloyati")
|
|
16
|
+
assert region is not None
|
|
17
|
+
assert region.capital == "Nurafshon"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_region_by_cyrillic_name():
|
|
21
|
+
region = get_region("Тошкент вилояти")
|
|
22
|
+
assert region is not None
|
|
23
|
+
assert region.name == "Toshkent viloyati"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_get_region_case_insensitive():
|
|
27
|
+
assert get_region("toshkent viloyati") == get_region("TOSHKENT VILOYATI")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_get_region_unknown_returns_none():
|
|
31
|
+
assert get_region("Mars") is None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_region_empty_returns_none():
|
|
35
|
+
assert get_region("") is None
|
|
36
|
+
assert get_region(" ") is None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_get_region_non_string_returns_none():
|
|
40
|
+
assert get_region(None) is None
|
|
41
|
+
assert get_region(123) is None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_all_capitals_present():
|
|
45
|
+
for region in list_regions():
|
|
46
|
+
assert region.capital
|
|
47
|
+
assert region.capital_cyrillic
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_plate_codes_match_validator_ranges():
|
|
51
|
+
codes = {region.plate_code for region in list_regions()}
|
|
52
|
+
assert codes == {"01", "10", "20", "25", "30", "40", "50", "60", "70", "75", "80", "85", "90", "95"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_unique_region_names():
|
|
56
|
+
names = [region.name for region in list_regions()]
|
|
57
|
+
assert len(names) == len(set(names))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_unique_cyrillic_names():
|
|
61
|
+
names = [region.name_cyrillic for region in list_regions()]
|
|
62
|
+
assert len(names) == len(set(names))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_qoraqalpogiston():
|
|
66
|
+
region = get_region("Qoraqalpog'iston Respublikasi")
|
|
67
|
+
assert region is not None
|
|
68
|
+
assert region.capital == "Nukus"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_toshkent_shahri():
|
|
72
|
+
region = get_region("Toshkent shahri")
|
|
73
|
+
assert region is not None
|
|
74
|
+
assert region.plate_code == "01"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_get_region_by_capital_not_required():
|
|
78
|
+
assert get_region("Nukus") is None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_immutable_region():
|
|
82
|
+
region = get_region("Xorazm viloyati")
|
|
83
|
+
with pytest.raises(Exception):
|
|
84
|
+
region.name = "changed"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_region_code_mapping():
|
|
88
|
+
assert get_region("Qashqadaryo viloyati").plate_code == "70"
|
|
89
|
+
assert get_region("Surxondaryo viloyati").plate_code == "75"
|
|
90
|
+
assert get_region("Buxoro viloyati").plate_code == "80"
|
|
91
|
+
assert get_region("Navoiy viloyati").plate_code == "85"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from uzkit.translit import OKINA, TUTUQ, to_cyrillic, to_latin
|
|
4
|
+
|
|
5
|
+
PAIRS = [
|
|
6
|
+
("Toshkent", "Тошкент"),
|
|
7
|
+
(f"O{OKINA}zbekiston", "Ўзбекистон"),
|
|
8
|
+
(f"G{OKINA}ijduvon", "Ғиждувон"),
|
|
9
|
+
(f"Qo{OKINA}qon", "Қўқон"),
|
|
10
|
+
("yaxshi", "яхши"),
|
|
11
|
+
("shahar", "шаҳар"),
|
|
12
|
+
("chiroyli", "чиройли"),
|
|
13
|
+
("yoz", "ёз"),
|
|
14
|
+
("Yevropa", "Европа"),
|
|
15
|
+
("Elektr", "Электр"),
|
|
16
|
+
("aeroport", "аэропорт"),
|
|
17
|
+
("SHAHAR", "ШАҲАР"),
|
|
18
|
+
("YOSH", "ЁШ"),
|
|
19
|
+
("Shahar", "Шаҳар"),
|
|
20
|
+
(f"ma{TUTUQ}no", "маъно"),
|
|
21
|
+
("", ""),
|
|
22
|
+
("123, 45!", "123, 45!"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("latin,cyrillic", PAIRS)
|
|
27
|
+
def test_to_cyrillic(latin, cyrillic):
|
|
28
|
+
assert to_cyrillic(latin) == cyrillic
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("latin,cyrillic", PAIRS)
|
|
32
|
+
def test_to_latin(latin, cyrillic):
|
|
33
|
+
assert to_latin(cyrillic) == latin
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("apos", ["'", "’", "ʻ", "‘", "`"])
|
|
37
|
+
def test_apostrophe_variants(apos):
|
|
38
|
+
assert to_cyrillic(f"o{apos}zbek") == "ўзбек"
|
|
39
|
+
assert to_cyrillic(f"ma{apos}no") == "маъно"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_quotes_at_word_edges_untouched():
|
|
43
|
+
assert to_cyrillic("'salom'") == "'салом'"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from uzkit.validators import (
|
|
4
|
+
MOBILE_OPERATOR_CODES,
|
|
5
|
+
is_valid_phone,
|
|
6
|
+
is_valid_plate,
|
|
7
|
+
normalize_phone,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------- telefon
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
"value",
|
|
14
|
+
[
|
|
15
|
+
"+998901234567",
|
|
16
|
+
"+998 90 123 45 67",
|
|
17
|
+
"+998 (90) 123-45-67",
|
|
18
|
+
"998901234567",
|
|
19
|
+
"901234567",
|
|
20
|
+
"90 123 45 67",
|
|
21
|
+
"+998.91.123.45.67",
|
|
22
|
+
" +998931234567 ",
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
def test_valid_phone_formats(value):
|
|
26
|
+
assert is_valid_phone(value) is True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("code", sorted(MOBILE_OPERATOR_CODES))
|
|
30
|
+
def test_every_operator_code_is_accepted(code):
|
|
31
|
+
assert is_valid_phone(f"+998{code}1234567")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize("code", ["90", "91", "93", "94", "95", "97", "99", "33", "88"])
|
|
35
|
+
def test_required_operator_codes_present(code):
|
|
36
|
+
assert code in MOBILE_OPERATOR_CODES
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
"value",
|
|
41
|
+
[
|
|
42
|
+
"",
|
|
43
|
+
" ",
|
|
44
|
+
"+99890123456", # juda qisqa
|
|
45
|
+
"+9989012345678", # juda uzun
|
|
46
|
+
"+998 10 123 45 67", # noma'lum operator kodi
|
|
47
|
+
"+998 00 123 45 67",
|
|
48
|
+
"+7 901 234 56 78", # boshqa davlat
|
|
49
|
+
"+901234567", # 9 xonali, lekin '+' bilan
|
|
50
|
+
"90123456", # 8 xonali
|
|
51
|
+
"+998 90 123 45 6a",
|
|
52
|
+
"++998901234567",
|
|
53
|
+
"+99890+1234567",
|
|
54
|
+
"abcdefghijkl",
|
|
55
|
+
"+٩٩٨٩٠١٢٣٤٥٦٧", # arab-hind raqamlari
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
def test_invalid_phone(value):
|
|
59
|
+
assert is_valid_phone(value) is False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize("value", [None, 998901234567, 90.1, [], b"+998901234567"])
|
|
63
|
+
def test_non_string_phone(value):
|
|
64
|
+
assert is_valid_phone(value) is False
|
|
65
|
+
assert normalize_phone(value) is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
("raw", "expected"),
|
|
70
|
+
[
|
|
71
|
+
("+998 90 123-45-67", "+998901234567"),
|
|
72
|
+
("998901234567", "+998901234567"),
|
|
73
|
+
("90 123 45 67", "+998901234567"),
|
|
74
|
+
("(93) 765-43-21", "+998937654321"),
|
|
75
|
+
("+998-33-111-22-33", "+998331112233"),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
def test_normalize_phone(raw, expected):
|
|
79
|
+
assert normalize_phone(raw) == expected
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_normalize_phone_invalid_returns_none():
|
|
83
|
+
assert normalize_phone("+998 10 123 45 67") is None
|
|
84
|
+
assert normalize_phone("hello") is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_normalize_is_idempotent():
|
|
88
|
+
once = normalize_phone("90 123 45 67")
|
|
89
|
+
assert normalize_phone(once) == once
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------- davlat raqami
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
"value",
|
|
95
|
+
[
|
|
96
|
+
"01 A 123 AA",
|
|
97
|
+
"01 123 AAA",
|
|
98
|
+
"01A123AA",
|
|
99
|
+
"01123AAA",
|
|
100
|
+
"01 a 123 aa",
|
|
101
|
+
" 30 B 456 CD ",
|
|
102
|
+
"95 Z 999 ZZ",
|
|
103
|
+
"80 777 XYZ",
|
|
104
|
+
"10 A 001 BC",
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
def test_valid_plate(value):
|
|
108
|
+
assert is_valid_plate(value) is True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize("code", [f"{i:02d}" for i in range(1, 100)])
|
|
112
|
+
def test_every_region_code_is_accepted(code):
|
|
113
|
+
assert is_valid_plate(f"{code} A 123 AA")
|
|
114
|
+
assert is_valid_plate(f"{code} 123 AAA")
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize("code", ["02", "31", "99"])
|
|
117
|
+
def test_selected_plate_codes_accepted(code):
|
|
118
|
+
assert is_valid_plate(f"{code} A 123 AA")
|
|
119
|
+
|
|
120
|
+
def test_plate_code_00_rejected():
|
|
121
|
+
assert is_valid_plate("00 A 123 AA") is False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize(
|
|
125
|
+
"value",
|
|
126
|
+
[
|
|
127
|
+
"",
|
|
128
|
+
" ",
|
|
129
|
+
"00 A 123 AA", # noma'lum viloyat kodi
|
|
130
|
+
"1 A 123 AA", # viloyat kodi bitta raqam
|
|
131
|
+
"001 A 123 AA",
|
|
132
|
+
"01 AA 123 AA",
|
|
133
|
+
"01 A 12 AA",
|
|
134
|
+
"01 A 1234 AA",
|
|
135
|
+
"01 A 123 A",
|
|
136
|
+
"01 A 123 AAA", # ikki format aralashgan
|
|
137
|
+
"01 12 AAA",
|
|
138
|
+
"01 1234 AAA",
|
|
139
|
+
"01 123 AA",
|
|
140
|
+
"01 123 AAAA",
|
|
141
|
+
"01 1 123 AA",
|
|
142
|
+
"A 01 123 AA",
|
|
143
|
+
"01-A-123-AA",
|
|
144
|
+
"01 A 123 AA", # ikkita bo'shliq
|
|
145
|
+
"01 А 123 АА", # kirill harflari
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
def test_invalid_plate(value):
|
|
149
|
+
assert is_valid_plate(value) is False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize("value", [None, 123, [], b"01 A 123 AA"])
|
|
153
|
+
def test_non_string_plate(value):
|
|
154
|
+
assert is_valid_plate(value) is False
|