pycustomrand 0.0.1__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.
- pycustomrand-0.0.1/LICENSE +21 -0
- pycustomrand-0.0.1/PKG-INFO +79 -0
- pycustomrand-0.0.1/README.md +64 -0
- pycustomrand-0.0.1/pycustomrand/__init__.py +68 -0
- pycustomrand-0.0.1/pycustomrand/custom_round.py +45 -0
- pycustomrand-0.0.1/pycustomrand/diagnostics.py +71 -0
- pycustomrand-0.0.1/pycustomrand/random_generator.py +296 -0
- pycustomrand-0.0.1/pycustomrand.egg-info/PKG-INFO +79 -0
- pycustomrand-0.0.1/pycustomrand.egg-info/SOURCES.txt +13 -0
- pycustomrand-0.0.1/pycustomrand.egg-info/dependency_links.txt +1 -0
- pycustomrand-0.0.1/pycustomrand.egg-info/top_level.txt +1 -0
- pycustomrand-0.0.1/pyproject.toml +22 -0
- pycustomrand-0.0.1/setup.cfg +4 -0
- pycustomrand-0.0.1/tests/test_random.py +68 -0
- pycustomrand-0.0.1/tests/test_round.py +107 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 nixsi
|
|
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.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycustomrand
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Библиотека для генерации псевдослучайных чисел на основе времени
|
|
5
|
+
Author-email: Ivan <vanyalebedev45@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/n1xsi/PyCustomRand
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/n1xsi/PyCustomRand/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# PyCustomRand
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/n1xsi/pycustomrand/commits/main)
|
|
20
|
+
|
|
21
|
+
> [!WARNING]
|
|
22
|
+
> *Библиотека находится на стадии разработки*.
|
|
23
|
+
>
|
|
24
|
+
> Также PyCustomRand **не является** криптографически стойкой библиотекой и **не предназначена** для использования в системах безопасности ❗
|
|
25
|
+
|
|
26
|
+
**PyCustomRand** — это небольшая Python-библиотека для генерации **псевдослучайных чисел**, основанная на алгоритме, использующем системное время с наносекундной точностью (time-based).
|
|
27
|
+
Проект также включает в себя кастомную реализацию математического (а не банковского) округления.
|
|
28
|
+
|
|
29
|
+
Это авторский проект создан для изучения алгоритмов и альтернативных подходов к генерации случайных чисел.
|
|
30
|
+
Меня не устраивало, что модуль `random` в Python генерирует недостаточно случайные числа ( *особенно при нескольких последовательных генерациях* ), а встроенный в Python модуль `round` довольно "грубо" округляет числа ( *`round(1.5)` = 2 и в то же время `round(2.5)` = 2* ), поэтому я решил написать *свою* библиотеку для генерации псевдослучайных чисел — более простую и с наибольшей энтропией.
|
|
31
|
+
|
|
32
|
+
## Основные возможности
|
|
33
|
+
Все функции PyCustomRand основаны на [оригинальной](https://docs.python.org/3/library/random.html) Python-библиотеке:
|
|
34
|
+
* Генерация случайных целых чисел заданной длины;
|
|
35
|
+
* Генерация случайных чисел с плавающей точкой в диапазоне `[0, 1)`;
|
|
36
|
+
* Выбор случайного элемента из списка;
|
|
37
|
+
* Перемешивание списка;
|
|
38
|
+
* А также: округление кастомной функцией `true_round`.
|
|
39
|
+
|
|
40
|
+
и т.д.
|
|
41
|
+
|
|
42
|
+
## Установка
|
|
43
|
+
*Пока что* установка через `pip` не поддерживается. Чтобы использовать библиотеку, **склонируйте** репозиторий:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/n1xsi/PyCustomRand.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Пример использования
|
|
50
|
+
```python
|
|
51
|
+
from pycustomrand import PseudoRandom
|
|
52
|
+
|
|
53
|
+
# Генерация 8-значного случайного числа
|
|
54
|
+
random_number = PseudoRandom.get_random_number(8)
|
|
55
|
+
print(f"Случайное число: {random_number}")
|
|
56
|
+
|
|
57
|
+
# Генерация float в диапазоне [0, 1)
|
|
58
|
+
random_float = PseudoRandom.random()
|
|
59
|
+
print(f"Случайный float: {random_float}")
|
|
60
|
+
|
|
61
|
+
# Выбор случайного элемента из списка
|
|
62
|
+
my_list = ['яблоко', 'банан', 'вишня']
|
|
63
|
+
random_choice = PseudoRandom.choice(my_list)
|
|
64
|
+
print(f"Случайный выбор: {random_choice}")
|
|
65
|
+
|
|
66
|
+
# Перемешивание списка
|
|
67
|
+
PseudoRandom.shuffle(my_list)
|
|
68
|
+
print(f"Перемешанный список: {my_list}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Планы на будущее
|
|
72
|
+
- [ ] Написание тестов.
|
|
73
|
+
- [ ] Публикация в PyPI.
|
|
74
|
+
- [ ] Доработка README.
|
|
75
|
+
|
|
76
|
+
## Контрибуция
|
|
77
|
+
🤝 Приветствуется вклад в развитие проекта: любые поправки/импрувы.
|
|
78
|
+
|
|
79
|
+
> **Форк → Коммит → Pull Request**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# PyCustomRand
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://github.com/n1xsi/pycustomrand/commits/main)
|
|
5
|
+
|
|
6
|
+
> [!WARNING]
|
|
7
|
+
> *Библиотека находится на стадии разработки*.
|
|
8
|
+
>
|
|
9
|
+
> Также PyCustomRand **не является** криптографически стойкой библиотекой и **не предназначена** для использования в системах безопасности ❗
|
|
10
|
+
|
|
11
|
+
**PyCustomRand** — это небольшая Python-библиотека для генерации **псевдослучайных чисел**, основанная на алгоритме, использующем системное время с наносекундной точностью (time-based).
|
|
12
|
+
Проект также включает в себя кастомную реализацию математического (а не банковского) округления.
|
|
13
|
+
|
|
14
|
+
Это авторский проект создан для изучения алгоритмов и альтернативных подходов к генерации случайных чисел.
|
|
15
|
+
Меня не устраивало, что модуль `random` в Python генерирует недостаточно случайные числа ( *особенно при нескольких последовательных генерациях* ), а встроенный в Python модуль `round` довольно "грубо" округляет числа ( *`round(1.5)` = 2 и в то же время `round(2.5)` = 2* ), поэтому я решил написать *свою* библиотеку для генерации псевдослучайных чисел — более простую и с наибольшей энтропией.
|
|
16
|
+
|
|
17
|
+
## Основные возможности
|
|
18
|
+
Все функции PyCustomRand основаны на [оригинальной](https://docs.python.org/3/library/random.html) Python-библиотеке:
|
|
19
|
+
* Генерация случайных целых чисел заданной длины;
|
|
20
|
+
* Генерация случайных чисел с плавающей точкой в диапазоне `[0, 1)`;
|
|
21
|
+
* Выбор случайного элемента из списка;
|
|
22
|
+
* Перемешивание списка;
|
|
23
|
+
* А также: округление кастомной функцией `true_round`.
|
|
24
|
+
|
|
25
|
+
и т.д.
|
|
26
|
+
|
|
27
|
+
## Установка
|
|
28
|
+
*Пока что* установка через `pip` не поддерживается. Чтобы использовать библиотеку, **склонируйте** репозиторий:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/n1xsi/PyCustomRand.git
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Пример использования
|
|
35
|
+
```python
|
|
36
|
+
from pycustomrand import PseudoRandom
|
|
37
|
+
|
|
38
|
+
# Генерация 8-значного случайного числа
|
|
39
|
+
random_number = PseudoRandom.get_random_number(8)
|
|
40
|
+
print(f"Случайное число: {random_number}")
|
|
41
|
+
|
|
42
|
+
# Генерация float в диапазоне [0, 1)
|
|
43
|
+
random_float = PseudoRandom.random()
|
|
44
|
+
print(f"Случайный float: {random_float}")
|
|
45
|
+
|
|
46
|
+
# Выбор случайного элемента из списка
|
|
47
|
+
my_list = ['яблоко', 'банан', 'вишня']
|
|
48
|
+
random_choice = PseudoRandom.choice(my_list)
|
|
49
|
+
print(f"Случайный выбор: {random_choice}")
|
|
50
|
+
|
|
51
|
+
# Перемешивание списка
|
|
52
|
+
PseudoRandom.shuffle(my_list)
|
|
53
|
+
print(f"Перемешанный список: {my_list}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Планы на будущее
|
|
57
|
+
- [ ] Написание тестов.
|
|
58
|
+
- [ ] Публикация в PyPI.
|
|
59
|
+
- [ ] Доработка README.
|
|
60
|
+
|
|
61
|
+
## Контрибуция
|
|
62
|
+
🤝 Приветствуется вклад в развитие проекта: любые поправки/импрувы.
|
|
63
|
+
|
|
64
|
+
> **Форк → Коммит → Pull Request**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Делает класс PseudoRandom доступным при импорте пакета
|
|
2
|
+
from .random_generator import PseudoRandom
|
|
3
|
+
|
|
4
|
+
# Версия пакета
|
|
5
|
+
__version__ = "0.0.1"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# -------------------- Алиасы --------------------
|
|
9
|
+
|
|
10
|
+
# Основные функции
|
|
11
|
+
set_seed = PseudoRandom.set_seed
|
|
12
|
+
gen_random_number = PseudoRandom.gen_random_number
|
|
13
|
+
random = PseudoRandom.random
|
|
14
|
+
|
|
15
|
+
# Целые числа
|
|
16
|
+
randrange = PseudoRandom.randrange
|
|
17
|
+
random_integer = PseudoRandom.random_integer
|
|
18
|
+
randint = PseudoRandom.random_integer
|
|
19
|
+
|
|
20
|
+
# Числа с плавающей точкой
|
|
21
|
+
random_float = PseudoRandom.random_float
|
|
22
|
+
|
|
23
|
+
# Байтовые функции
|
|
24
|
+
random_bytes = PseudoRandom.random_bytes
|
|
25
|
+
|
|
26
|
+
# Последовательности
|
|
27
|
+
choice = PseudoRandom.choice
|
|
28
|
+
choices = PseudoRandom.choices
|
|
29
|
+
shuffle = PseudoRandom.shuffle
|
|
30
|
+
sample = PseudoRandom.sample
|
|
31
|
+
|
|
32
|
+
# Распределения
|
|
33
|
+
triangular = PseudoRandom.triangular
|
|
34
|
+
gauss = PseudoRandom.gauss
|
|
35
|
+
expovariate = PseudoRandom.expovariate
|
|
36
|
+
binomial = PseudoRandom.binomialvariate
|
|
37
|
+
|
|
38
|
+
# Утилиты
|
|
39
|
+
random_bool = PseudoRandom.random_bool
|
|
40
|
+
random_uuid4 = PseudoRandom.random_uuid4
|
|
41
|
+
random_color_hex = PseudoRandom.random_color_hex
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------
|
|
44
|
+
|
|
45
|
+
# Для "from pycustomrand import *"
|
|
46
|
+
__all__ = [
|
|
47
|
+
"PseudoRandom",
|
|
48
|
+
"true_round",
|
|
49
|
+
"random",
|
|
50
|
+
"set_seed",
|
|
51
|
+
"gen_random_number",
|
|
52
|
+
"randrange",
|
|
53
|
+
"random_integer",
|
|
54
|
+
"randint",
|
|
55
|
+
"random_float",
|
|
56
|
+
"random_bytes",
|
|
57
|
+
"choice",
|
|
58
|
+
"choices",
|
|
59
|
+
"shuffle",
|
|
60
|
+
"sample",
|
|
61
|
+
"triangular",
|
|
62
|
+
"gauss",
|
|
63
|
+
"expovariate",
|
|
64
|
+
"binomial",
|
|
65
|
+
"random_bool",
|
|
66
|
+
"random_uuid4",
|
|
67
|
+
"random_color_hex"
|
|
68
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
def cascade_round(digits: list[int], index: int) -> None:
|
|
2
|
+
"""Вспомогательная функция для каскадного округления цифр в списке digits, начиная с позиции index."""
|
|
3
|
+
# Если цифра на текущей позиции равна 10: устанавливаем её в 0 и увеличиваем предыдущую цифру на 1
|
|
4
|
+
if digits[index] == 10:
|
|
5
|
+
digits[index] = 0
|
|
6
|
+
if index - 1 >= 0:
|
|
7
|
+
digits[index - 1] += 1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def true_round(number: int | float, length: int = 0) -> int | float:
|
|
11
|
+
"""Функция для честного округления числа number до заданного количества знаков после запятой length."""
|
|
12
|
+
# Если пришёл int - сразу возвращаем int
|
|
13
|
+
if isinstance(number, int):
|
|
14
|
+
return number
|
|
15
|
+
|
|
16
|
+
# Получение цифр после запятой заданного числа
|
|
17
|
+
number_parts = str(number).split('.')
|
|
18
|
+
# Определение знака числа
|
|
19
|
+
sign = -1 if number_parts[0][0] == '-' else 1
|
|
20
|
+
# Создаём список цифр из числа (digits[0] - целая часть со знаком, digits[1] - дробная)
|
|
21
|
+
digits = [abs(int(number_parts[0]))] + list(map(int, number_parts[1]))
|
|
22
|
+
|
|
23
|
+
# Начало прохода: по цифрам в обратном порядке, начиная с той, что после нужного знака округления
|
|
24
|
+
start = len(digits) - 1 - (1 if length == 0 else 0)
|
|
25
|
+
# Конец прохода: до первой цифры после запятой включительно (или до конца числа, если length == 0)
|
|
26
|
+
end = length - 1 - (-2 if length == 0 else -1)
|
|
27
|
+
|
|
28
|
+
# Проход по цифрам в обратном порядке (логика округления)
|
|
29
|
+
for i in range(start, end, -1):
|
|
30
|
+
# Если цифра на текущей позиции >= 5, то +1 к предыдущей цифре (правило округления)
|
|
31
|
+
if digits[i] >= 5:
|
|
32
|
+
digits[i-1] += 1
|
|
33
|
+
digits[i] = 0
|
|
34
|
+
# Обработка каскадного округления
|
|
35
|
+
cascade_round(digits, i - 1)
|
|
36
|
+
# Доп. обработка каскадного округления после цикла (на случай, если первая цифра после запятой стала 10)
|
|
37
|
+
cascade_round(digits, 1)
|
|
38
|
+
|
|
39
|
+
# Если кол-во знаков после запятой (length) не было задано
|
|
40
|
+
if length == 0:
|
|
41
|
+
# Тогда округляем до целого числа (+1 к числу, если первая цифра после запятой >= 5)
|
|
42
|
+
return (int(digits[0]) + (1 if digits[1] >= 5 else 0)) * sign
|
|
43
|
+
else:
|
|
44
|
+
# Иначе формируем и возвращаем число с заданным кол-вом знаков после запятой
|
|
45
|
+
return float(f"{digits[0]}.{''.join(map(str, digits[1:length + 1]))}") * sign
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from time import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def check_distribution(count: int = 1000, buckets: int = 10):
|
|
7
|
+
"""
|
|
8
|
+
Декоратор для проверки равномерности распределения генератора.
|
|
9
|
+
|
|
10
|
+
count - сколько раз запустить генератор.
|
|
11
|
+
buckets - на сколько частей разбить диапазон [0, 1).
|
|
12
|
+
"""
|
|
13
|
+
def decorator(func):
|
|
14
|
+
@wraps(func)
|
|
15
|
+
def wrapper(*args, **kwargs):
|
|
16
|
+
print(f"===== Запуск анализа распределения ({count} итераций) =====")
|
|
17
|
+
start_time = time()
|
|
18
|
+
previous_time = start_time
|
|
19
|
+
|
|
20
|
+
# Словарь для подсчета попаданий
|
|
21
|
+
stats = defaultdict(int)
|
|
22
|
+
|
|
23
|
+
for i in range(count):
|
|
24
|
+
# Каждые 5 секунд вывод текущей итерации
|
|
25
|
+
current_time = time()
|
|
26
|
+
if current_time - previous_time > 5:
|
|
27
|
+
print(f"Нынешняя итерация: {i}")
|
|
28
|
+
previous_time = current_time
|
|
29
|
+
|
|
30
|
+
val = func(*args, **kwargs)
|
|
31
|
+
# Превращение числа 0.0-1.0 в номер корзины (0, 1, ..., buckets-1)
|
|
32
|
+
# int() для корзин равного размера
|
|
33
|
+
bucket_index = int(val * buckets)
|
|
34
|
+
|
|
35
|
+
# Защита от граничного случая, если выпадет ровно 1.0
|
|
36
|
+
if bucket_index == buckets:
|
|
37
|
+
bucket_index -= 1
|
|
38
|
+
|
|
39
|
+
stats[bucket_index] += 1
|
|
40
|
+
|
|
41
|
+
elapsed = time() - start_time
|
|
42
|
+
print(f"Генерация завершена за {elapsed:.4f} сек.\n")
|
|
43
|
+
|
|
44
|
+
# Анализ результатов
|
|
45
|
+
expected_percent = 100 / buckets
|
|
46
|
+
print(f"{'Корзина':<10} | {'Кол-во':<10} | {'%':<10} | {'Отклонение':<10}")
|
|
47
|
+
print("=" * 50)
|
|
48
|
+
|
|
49
|
+
max_diff = 0
|
|
50
|
+
|
|
51
|
+
for k in sorted(stats.keys()):
|
|
52
|
+
v = stats[k]
|
|
53
|
+
percent = (v / count) * 100
|
|
54
|
+
diff = abs(percent - expected_percent)
|
|
55
|
+
if diff > max_diff:
|
|
56
|
+
max_diff = diff
|
|
57
|
+
|
|
58
|
+
print(f"{k:<10} | {v:<10} | {percent:<10.2f}% | {diff:<10.2f}%")
|
|
59
|
+
|
|
60
|
+
print("=" * 50)
|
|
61
|
+
print(f"Максимальное отклонение: {max_diff:.4f}%")
|
|
62
|
+
if max_diff < 1.0:
|
|
63
|
+
print(">> РЕЗУЛЬТАТ: Отличное равномерное распределение!")
|
|
64
|
+
else:
|
|
65
|
+
print(">> РЕЗУЛЬТАТ: Есть перекосы (вероятно, мала выборка или проблема в алгоритме).")
|
|
66
|
+
|
|
67
|
+
# Возврат последнего сгенерированного числа, чтобы не ломать логику программы
|
|
68
|
+
return val
|
|
69
|
+
|
|
70
|
+
return wrapper
|
|
71
|
+
return decorator
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from .custom_round import true_round
|
|
2
|
+
from math import sqrt, log, cos, pi
|
|
3
|
+
from time import sleep, time_ns
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PseudoRandom:
|
|
8
|
+
# Переменная класса для хранения seed генератора псевдослучайных чисел
|
|
9
|
+
_seed = None
|
|
10
|
+
|
|
11
|
+
# -------------------- Основные функции генерации случайных чисел --------------------
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def set_seed(cls, seed: Any = None) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Установка нового значения seed.
|
|
17
|
+
|
|
18
|
+
seed - любой объект, который преобразуется в строку. Если None - сброс на время (time_ns()).
|
|
19
|
+
"""
|
|
20
|
+
if seed is not None:
|
|
21
|
+
cls._seed = sum(map(ord, str(seed)))
|
|
22
|
+
else:
|
|
23
|
+
cls._seed = None
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _get_next_seed_state(current_seed: int) -> int:
|
|
27
|
+
"""
|
|
28
|
+
Вспомогательная функция - меняет состояние зерна с помощью линейного конгруэнтного метода.
|
|
29
|
+
(Константы взяты из Borland C/C++ runtime library.)
|
|
30
|
+
"""
|
|
31
|
+
return (current_seed * 22695477 + 1) & 0xFFFFFFFF
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def gen_random_number(cls, length: int = 1) -> int:
|
|
35
|
+
"""Генерация псевдослучайного числа заданной длины."""
|
|
36
|
+
number = ''
|
|
37
|
+
while len(number) != length:
|
|
38
|
+
# Определение источника энтропии (время или seed)
|
|
39
|
+
if cls._seed is not None:
|
|
40
|
+
is_seeded = True
|
|
41
|
+
current_entropy = cls._seed
|
|
42
|
+
# Обновление seed для следующей итерации (цифры), иначе результат будет одинаковым
|
|
43
|
+
cls._seed = cls._get_next_seed_state(cls._seed)
|
|
44
|
+
else:
|
|
45
|
+
is_seeded = False
|
|
46
|
+
current_entropy = time_ns()
|
|
47
|
+
|
|
48
|
+
# Генерация псевдослучайного числа на основе текущей энтропии и "волшебных" математических операций
|
|
49
|
+
calc_base = int((current_entropy * 1.71) / 0.8)
|
|
50
|
+
reversed_base = int(str(calc_base)[::-1])
|
|
51
|
+
magic_result = ((reversed_base ** 0.5) * 7) / 9
|
|
52
|
+
|
|
53
|
+
# Добавление последней цифры результата
|
|
54
|
+
number += str(int(magic_result))[-1]
|
|
55
|
+
|
|
56
|
+
# Если нет seed, нужна задержка, чтобы время изменилось (т.к. entropy - время)
|
|
57
|
+
if not is_seeded:
|
|
58
|
+
sleep(0.0001)
|
|
59
|
+
|
|
60
|
+
return int(number)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def random() -> float:
|
|
64
|
+
"""Возвращает случайное число с плавающей точкой в диапазоне [0.0, 1.0)."""
|
|
65
|
+
raw_int = PseudoRandom.gen_random_number(16)
|
|
66
|
+
raw_str = str(raw_int).zfill(16)
|
|
67
|
+
return float("0." + raw_str)
|
|
68
|
+
|
|
69
|
+
# -------------------- Числовые функции --------------------
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def randrange(start: int, stop: int = None, step: int = 1) -> int:
|
|
73
|
+
"""
|
|
74
|
+
Возвращает случайное число из диапазона [start, stop).
|
|
75
|
+
Верхняя граница НЕ включается. Можно указывать один аргумент.
|
|
76
|
+
|
|
77
|
+
Примеры:
|
|
78
|
+
randrange(10) -> от 0 до 9
|
|
79
|
+
randrange(1, 10) -> от 1 до 9
|
|
80
|
+
randrange(0, 10, 2) -> чётное число от 0 до 8
|
|
81
|
+
"""
|
|
82
|
+
if stop is None:
|
|
83
|
+
# Если передан один аргумент, например randrange(10), то считаем его за stop, а start за 0
|
|
84
|
+
stop = start
|
|
85
|
+
start = 0
|
|
86
|
+
|
|
87
|
+
range_object = range(start, stop, step)
|
|
88
|
+
|
|
89
|
+
if not range_object:
|
|
90
|
+
raise ValueError("Пустой диапазон для генерации")
|
|
91
|
+
|
|
92
|
+
return range_object[int(PseudoRandom.random() * len(range_object))]
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def random_integer(start: int, end: int = None, step: int = 1) -> int:
|
|
96
|
+
"""
|
|
97
|
+
Возвращает случайное число из диапазона [start, end].
|
|
98
|
+
Включительны и обязательны обе границы.
|
|
99
|
+
|
|
100
|
+
step (опциональный аргумент) - число должно делиться на step (относительно start)
|
|
101
|
+
"""
|
|
102
|
+
if step == 0:
|
|
103
|
+
raise ValueError("Step (шаг) не может быть равен 0")
|
|
104
|
+
|
|
105
|
+
# Длина диапазона
|
|
106
|
+
width = end - start
|
|
107
|
+
|
|
108
|
+
# Количество шагов = (разница / шаг) + 1
|
|
109
|
+
n_steps = int(width / step) + 1
|
|
110
|
+
|
|
111
|
+
if n_steps <= 0:
|
|
112
|
+
raise ValueError("Неверные границы диапазона для заданного шага")
|
|
113
|
+
|
|
114
|
+
# Выбор случайного индекса шага
|
|
115
|
+
random_step_index = int(PseudoRandom.random() * n_steps)
|
|
116
|
+
|
|
117
|
+
return start + (random_step_index * step)
|
|
118
|
+
|
|
119
|
+
# -------------------- Функции для чисел с плавающей точкой --------------------
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def random_float(start: int | float, end: int | float, digits: int = None) -> float:
|
|
123
|
+
"""
|
|
124
|
+
Возвращает случайноe число с плавающей точкой из диапазона [start, end)
|
|
125
|
+
или [start, end] в зависимости от округления (при наличии digits).
|
|
126
|
+
|
|
127
|
+
start, end - могут быть как целыми числами, так и числами с плавающей точкой.
|
|
128
|
+
digits - количество знаков после запятой в возвращаемом числе. Если None - без округления.
|
|
129
|
+
"""
|
|
130
|
+
result = (end-start) * (PseudoRandom.random()) + start
|
|
131
|
+
if digits is not None:
|
|
132
|
+
return true_round(result, digits)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def triangular(low: float = 0.0, high: float = 1.0, mode: float = None) -> float:
|
|
137
|
+
"""
|
|
138
|
+
Возвращает случайное число с треугольным распределением в диапазоне [low, high].
|
|
139
|
+
Чаще всего выпадает значение около mode (вершина треугольника).
|
|
140
|
+
"""
|
|
141
|
+
uniform = PseudoRandom.random()
|
|
142
|
+
cutoff = 0.5 if mode is None else (mode - low) / (high - low)
|
|
143
|
+
if uniform > cutoff:
|
|
144
|
+
uniform = 1.0 - uniform
|
|
145
|
+
cutoff = 1.0 - cutoff
|
|
146
|
+
low, high = high, low
|
|
147
|
+
return low + (high - low) * (uniform * cutoff) ** 0.5
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def gauss(mu: float = 0.0, sigma: float = 1.0) -> float:
|
|
151
|
+
"""
|
|
152
|
+
Возвращает случайное число с нормальным (гауссовым) распределением.
|
|
153
|
+
Алгоритм Бокса-Мюллера.
|
|
154
|
+
|
|
155
|
+
mu - среднее значение, центр колокола (математическое ожидание).
|
|
156
|
+
sigma - стандартное отклонение, ширина колокола.
|
|
157
|
+
"""
|
|
158
|
+
u1 = PseudoRandom.random()
|
|
159
|
+
u2 = PseudoRandom.random()
|
|
160
|
+
z0 = sqrt(-2.0 * log(u1)) * cos(2.0 * pi * u2)
|
|
161
|
+
return z0 * sigma + mu
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def expovariate(lambd: float = 1.0) -> float:
|
|
165
|
+
"""
|
|
166
|
+
Возвращает случайное число с экспоненциальным распределением.
|
|
167
|
+
|
|
168
|
+
lambd - параметр интенсивности (должен быть не 0).
|
|
169
|
+
"""
|
|
170
|
+
return -log(1 - PseudoRandom.random()) / lambd
|
|
171
|
+
|
|
172
|
+
# -------------------- Байтовые функции --------------------
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def random_bytes(count: int) -> bytes:
|
|
176
|
+
"""Возвращает случайные байты в количестве count."""
|
|
177
|
+
return bytes([PseudoRandom.random_integer(0, 255) for _ in range(count)])
|
|
178
|
+
|
|
179
|
+
# -------------------- Функции для последовательностей --------------------
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def choice(array: list[Any]) -> Any:
|
|
183
|
+
"""Возвращает случайно выбранный элемент из массива."""
|
|
184
|
+
if not array:
|
|
185
|
+
return None
|
|
186
|
+
index = PseudoRandom.randrange(len(array))
|
|
187
|
+
return array[index]
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def choices(array: list[Any], k: int, weights: list[int] = None) -> list[Any]:
|
|
191
|
+
"""
|
|
192
|
+
Возвращает список из k случайных элементов из массива (с повторениями).
|
|
193
|
+
|
|
194
|
+
weights - список с весами для каждого элемента массива (соответствует по индексу).
|
|
195
|
+
Если None - все элементы считаются равными (по 1 весу).
|
|
196
|
+
"""
|
|
197
|
+
if weights is None:
|
|
198
|
+
weights = [1] * len(array)
|
|
199
|
+
|
|
200
|
+
if len(array) != len(weights):
|
|
201
|
+
raise ValueError("Длина массива и длина weights должны быть равны")
|
|
202
|
+
|
|
203
|
+
weighted_array = []
|
|
204
|
+
for item, weight in zip(array, weights):
|
|
205
|
+
weighted_array.extend([item] * weight)
|
|
206
|
+
|
|
207
|
+
return [PseudoRandom.choice(weighted_array) for _ in range(k)]
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def shuffle(array: list[Any]) -> None:
|
|
211
|
+
"""Перемешивает массив на месте."""
|
|
212
|
+
for i in range(len(array) - 1, 0, -1):
|
|
213
|
+
j = PseudoRandom.randrange(i + 1)
|
|
214
|
+
array[i], array[j] = array[j], array[i]
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def sample(array: list[Any], k: int, counts: list[int] = None) -> list[Any]:
|
|
218
|
+
"""
|
|
219
|
+
Возвращает список из k уникальных случайных элементов из массива.
|
|
220
|
+
|
|
221
|
+
counts - список с количеством повторений для каждого элемента массива (соответствует по индексу).
|
|
222
|
+
Если None - все элементы считаются равными (по 1 повторению).
|
|
223
|
+
"""
|
|
224
|
+
if counts is None:
|
|
225
|
+
counts = [1] * len(array)
|
|
226
|
+
|
|
227
|
+
if len(array) != len(counts):
|
|
228
|
+
raise ValueError("Длина массива и длина counts должны быть равны")
|
|
229
|
+
|
|
230
|
+
weighted_array = []
|
|
231
|
+
for item, count in zip(array, counts):
|
|
232
|
+
weighted_array.extend([item] * count)
|
|
233
|
+
|
|
234
|
+
if k > len(set(weighted_array)):
|
|
235
|
+
raise ValueError("k не может быть больше количества уникальных элементов в массиве с учётом counts")
|
|
236
|
+
|
|
237
|
+
result = []
|
|
238
|
+
while len(result) < k:
|
|
239
|
+
choice = PseudoRandom.choice(weighted_array)
|
|
240
|
+
if choice not in result:
|
|
241
|
+
result.append(choice)
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
# -------------------- Дискретные функции --------------------
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def binomialvariate(n: int = 1, p: float = 0.5) -> int:
|
|
249
|
+
"""
|
|
250
|
+
Возвращает случайное число, распределённое по биномиальному закону.
|
|
251
|
+
Простейшая реализация, неоптимизированная.
|
|
252
|
+
|
|
253
|
+
n - количество испытаний (целое число >= 0).
|
|
254
|
+
p - вероятность успеха в каждом испытании (0.0 <= p <= 1.0).
|
|
255
|
+
"""
|
|
256
|
+
if n < 0:
|
|
257
|
+
raise ValueError("Количество испытаний n не может быть отрицательным")
|
|
258
|
+
if not (0.0 <= p <= 1.0):
|
|
259
|
+
raise ValueError("Вероятность p должна быть в диапазоне [0, 1]")
|
|
260
|
+
|
|
261
|
+
return sum([PseudoRandom.random() < p for _ in range(n)])
|
|
262
|
+
|
|
263
|
+
# -------------------- Вспомогательные функции --------------------
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def random_bool(true_chance: float = 0.5) -> bool:
|
|
267
|
+
"""Возвращает True/False с вероятностью true_chance."""
|
|
268
|
+
return PseudoRandom.random() < true_chance
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def random_uuid4() -> str:
|
|
272
|
+
"""
|
|
273
|
+
Возвращает случайный UUID версии 4
|
|
274
|
+
|
|
275
|
+
Пример: 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
276
|
+
"""
|
|
277
|
+
# Генерирация 32 hex-цифры
|
|
278
|
+
chars = [hex(PseudoRandom.random_integer(0, 15))[2:]
|
|
279
|
+
for _ in range(32)]
|
|
280
|
+
|
|
281
|
+
# Согласно стандарту UUID v4:
|
|
282
|
+
chars[12] = '4' # 13-й символ всегда '4'
|
|
283
|
+
# 17-й символ должен быть одним из '8', '9', 'a', 'b'
|
|
284
|
+
chars[16] = hex(PseudoRandom.choice([8, 9, 10, 11]))[2:]
|
|
285
|
+
|
|
286
|
+
return f"{''.join(chars[:8])}-{''.join(chars[8:12])}-{''.join(chars[12:16])}-{''.join(chars[16:20])}-{''.join(chars[20:])}"
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def random_color_hex() -> str:
|
|
290
|
+
"""
|
|
291
|
+
Возвращает случайный цвет в формате hex.
|
|
292
|
+
|
|
293
|
+
Пример: '#ff0000'
|
|
294
|
+
"""
|
|
295
|
+
val = PseudoRandom.random_integer(0, 0xFFFFFF)
|
|
296
|
+
return f"#{hex(val)[2:].zfill(6)}"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycustomrand
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Библиотека для генерации псевдослучайных чисел на основе времени
|
|
5
|
+
Author-email: Ivan <vanyalebedev45@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/n1xsi/PyCustomRand
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/n1xsi/PyCustomRand/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# PyCustomRand
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/n1xsi/pycustomrand/commits/main)
|
|
20
|
+
|
|
21
|
+
> [!WARNING]
|
|
22
|
+
> *Библиотека находится на стадии разработки*.
|
|
23
|
+
>
|
|
24
|
+
> Также PyCustomRand **не является** криптографически стойкой библиотекой и **не предназначена** для использования в системах безопасности ❗
|
|
25
|
+
|
|
26
|
+
**PyCustomRand** — это небольшая Python-библиотека для генерации **псевдослучайных чисел**, основанная на алгоритме, использующем системное время с наносекундной точностью (time-based).
|
|
27
|
+
Проект также включает в себя кастомную реализацию математического (а не банковского) округления.
|
|
28
|
+
|
|
29
|
+
Это авторский проект создан для изучения алгоритмов и альтернативных подходов к генерации случайных чисел.
|
|
30
|
+
Меня не устраивало, что модуль `random` в Python генерирует недостаточно случайные числа ( *особенно при нескольких последовательных генерациях* ), а встроенный в Python модуль `round` довольно "грубо" округляет числа ( *`round(1.5)` = 2 и в то же время `round(2.5)` = 2* ), поэтому я решил написать *свою* библиотеку для генерации псевдослучайных чисел — более простую и с наибольшей энтропией.
|
|
31
|
+
|
|
32
|
+
## Основные возможности
|
|
33
|
+
Все функции PyCustomRand основаны на [оригинальной](https://docs.python.org/3/library/random.html) Python-библиотеке:
|
|
34
|
+
* Генерация случайных целых чисел заданной длины;
|
|
35
|
+
* Генерация случайных чисел с плавающей точкой в диапазоне `[0, 1)`;
|
|
36
|
+
* Выбор случайного элемента из списка;
|
|
37
|
+
* Перемешивание списка;
|
|
38
|
+
* А также: округление кастомной функцией `true_round`.
|
|
39
|
+
|
|
40
|
+
и т.д.
|
|
41
|
+
|
|
42
|
+
## Установка
|
|
43
|
+
*Пока что* установка через `pip` не поддерживается. Чтобы использовать библиотеку, **склонируйте** репозиторий:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/n1xsi/PyCustomRand.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Пример использования
|
|
50
|
+
```python
|
|
51
|
+
from pycustomrand import PseudoRandom
|
|
52
|
+
|
|
53
|
+
# Генерация 8-значного случайного числа
|
|
54
|
+
random_number = PseudoRandom.get_random_number(8)
|
|
55
|
+
print(f"Случайное число: {random_number}")
|
|
56
|
+
|
|
57
|
+
# Генерация float в диапазоне [0, 1)
|
|
58
|
+
random_float = PseudoRandom.random()
|
|
59
|
+
print(f"Случайный float: {random_float}")
|
|
60
|
+
|
|
61
|
+
# Выбор случайного элемента из списка
|
|
62
|
+
my_list = ['яблоко', 'банан', 'вишня']
|
|
63
|
+
random_choice = PseudoRandom.choice(my_list)
|
|
64
|
+
print(f"Случайный выбор: {random_choice}")
|
|
65
|
+
|
|
66
|
+
# Перемешивание списка
|
|
67
|
+
PseudoRandom.shuffle(my_list)
|
|
68
|
+
print(f"Перемешанный список: {my_list}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Планы на будущее
|
|
72
|
+
- [ ] Написание тестов.
|
|
73
|
+
- [ ] Публикация в PyPI.
|
|
74
|
+
- [ ] Доработка README.
|
|
75
|
+
|
|
76
|
+
## Контрибуция
|
|
77
|
+
🤝 Приветствуется вклад в развитие проекта: любые поправки/импрувы.
|
|
78
|
+
|
|
79
|
+
> **Форк → Коммит → Pull Request**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pycustomrand/__init__.py
|
|
5
|
+
pycustomrand/custom_round.py
|
|
6
|
+
pycustomrand/diagnostics.py
|
|
7
|
+
pycustomrand/random_generator.py
|
|
8
|
+
pycustomrand.egg-info/PKG-INFO
|
|
9
|
+
pycustomrand.egg-info/SOURCES.txt
|
|
10
|
+
pycustomrand.egg-info/dependency_links.txt
|
|
11
|
+
pycustomrand.egg-info/top_level.txt
|
|
12
|
+
tests/test_random.py
|
|
13
|
+
tests/test_round.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pycustomrand
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pycustomrand"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Ivan", email="vanyalebedev45@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Библиотека для генерации псевдослучайных чисел на основе времени"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/n1xsi/PyCustomRand"
|
|
22
|
+
"Bug Tracker" = "https://github.com/n1xsi/PyCustomRand/issues"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pycustomrand.random_generator import PseudoRandom
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestPseudoRandom(unittest.TestCase):
|
|
6
|
+
|
|
7
|
+
def setUp(self):
|
|
8
|
+
"""Выполняется перед каждым тестом"""
|
|
9
|
+
# Сброс сида перед каждым тестом (для чистоты)
|
|
10
|
+
PseudoRandom.set_seed(None)
|
|
11
|
+
|
|
12
|
+
def test_reproducibility_with_seed(self):
|
|
13
|
+
"""Главный тест: проверка работы Seed"""
|
|
14
|
+
seed_value = "TestSeed123"
|
|
15
|
+
|
|
16
|
+
# Запуск 1
|
|
17
|
+
PseudoRandom.set_seed(seed_value)
|
|
18
|
+
val1 = PseudoRandom.random()
|
|
19
|
+
val2 = PseudoRandom.random_integer(0, 100)
|
|
20
|
+
|
|
21
|
+
# Запуск 2 (с тем же сидом)
|
|
22
|
+
PseudoRandom.set_seed(seed_value)
|
|
23
|
+
val1_again = PseudoRandom.random()
|
|
24
|
+
val2_again = PseudoRandom.random_integer(0, 100)
|
|
25
|
+
|
|
26
|
+
self.assertEqual(val1, val1_again, "Seed не работает: float значения разные")
|
|
27
|
+
self.assertEqual(val2, val2_again, "Seed не работает: integer значения разные")
|
|
28
|
+
|
|
29
|
+
def test_randrange_bounds(self):
|
|
30
|
+
"""Проверка границ randrange"""
|
|
31
|
+
for _ in range(100):
|
|
32
|
+
val = PseudoRandom.randrange(0, 5)
|
|
33
|
+
self.assertGreaterEqual(val, 0)
|
|
34
|
+
self.assertLess(val, 5) # Строго меньше 5
|
|
35
|
+
|
|
36
|
+
def test_random_integer_bounds(self):
|
|
37
|
+
"""Проверка границ random_integer (включительно)"""
|
|
38
|
+
found_max = False
|
|
39
|
+
for _ in range(200):
|
|
40
|
+
val = PseudoRandom.random_integer(1, 3)
|
|
41
|
+
self.assertGreaterEqual(val, 1)
|
|
42
|
+
self.assertLessEqual(val, 3)
|
|
43
|
+
if val == 3:
|
|
44
|
+
found_max = True
|
|
45
|
+
# Надежда на то, что за 200 попыток хоть раз выпадет 3 (верхняя граница)
|
|
46
|
+
self.assertTrue(found_max, "Верхняя граница random_integer не достигается")
|
|
47
|
+
|
|
48
|
+
def test_choice_error(self):
|
|
49
|
+
"""Проверка, что пустой массив возвращает None"""
|
|
50
|
+
self.assertIsNone(PseudoRandom.choice([]))
|
|
51
|
+
|
|
52
|
+
def test_random_bytes_length(self):
|
|
53
|
+
"""Проверка длины байтов"""
|
|
54
|
+
b = PseudoRandom.random_bytes(10)
|
|
55
|
+
self.assertEqual(len(b), 10)
|
|
56
|
+
self.assertIsInstance(b, bytes)
|
|
57
|
+
|
|
58
|
+
def test_uuid4_format(self):
|
|
59
|
+
"""Проверка формата UUID (если ты добавил эту функцию)"""
|
|
60
|
+
if hasattr(PseudoRandom, 'uuid4'):
|
|
61
|
+
uuid = PseudoRandom.uuid4()
|
|
62
|
+
self.assertEqual(len(uuid), 36)
|
|
63
|
+
self.assertEqual(uuid[14], '4') # Версия UUID всегда 4
|
|
64
|
+
self.assertIn(uuid[19], ['8', '9', 'a', 'b']) # Вариант UUID
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
unittest.main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from pycustomrand.custom_round import true_round
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestTrueRound(unittest.TestCase):
|
|
6
|
+
|
|
7
|
+
# -------------------- Базовые тесты --------------------
|
|
8
|
+
|
|
9
|
+
def test_integers(self):
|
|
10
|
+
"""Проверка: целые числа не должны меняться."""
|
|
11
|
+
self.assertEqual(true_round(5), 5)
|
|
12
|
+
self.assertEqual(true_round(0), 0)
|
|
13
|
+
self.assertEqual(true_round(100), 100)
|
|
14
|
+
|
|
15
|
+
def test_basic_rounding_down(self):
|
|
16
|
+
"""Проверка: округление вниз."""
|
|
17
|
+
self.assertEqual(true_round(1.1), 1)
|
|
18
|
+
self.assertEqual(true_round(1.4), 1)
|
|
19
|
+
self.assertEqual(true_round(1.4999), 2)
|
|
20
|
+
|
|
21
|
+
def test_basic_rounding_up(self):
|
|
22
|
+
"""Проверка: округление вверх (числа > 0.5)."""
|
|
23
|
+
self.assertEqual(true_round(1.6), 2)
|
|
24
|
+
self.assertEqual(true_round(1.9), 2)
|
|
25
|
+
self.assertEqual(true_round(0.99), 1)
|
|
26
|
+
|
|
27
|
+
def test_rounding_half_up(self):
|
|
28
|
+
"""
|
|
29
|
+
ГЛАВНАЯ ПРОВЕРКА: Округление 0.5
|
|
30
|
+
В стандартном Python round(2.5) == 2 (банковское округление).
|
|
31
|
+
Функция true_round должна давать 3 (математическое округление).
|
|
32
|
+
"""
|
|
33
|
+
self.assertEqual(true_round(0.5), 1)
|
|
34
|
+
self.assertEqual(true_round(1.5), 2)
|
|
35
|
+
self.assertEqual(true_round(2.5), 3)
|
|
36
|
+
self.assertEqual(true_round(3.5), 4)
|
|
37
|
+
|
|
38
|
+
def test_precision_1_digit(self):
|
|
39
|
+
"""Проверка: округление до 1 знака"""
|
|
40
|
+
self.assertEqual(true_round(0.14, 1), 0.1)
|
|
41
|
+
self.assertEqual(true_round(0.15, 1), 0.2)
|
|
42
|
+
self.assertEqual(true_round(0.16, 1), 0.2)
|
|
43
|
+
|
|
44
|
+
def test_precision_cascade(self):
|
|
45
|
+
"""
|
|
46
|
+
Проверка каскадного округления.
|
|
47
|
+
Пример: 1.99 при округлении до 1 знака должно стать 2.0
|
|
48
|
+
"""
|
|
49
|
+
self.assertEqual(true_round(1.99, 1), 2.0)
|
|
50
|
+
self.assertEqual(true_round(9.99, 1), 10.0)
|
|
51
|
+
|
|
52
|
+
# Проверка сложного случая: 0.445 -> 0.45 -> 0.5 (при округлении до 1 знака)
|
|
53
|
+
self.assertEqual(true_round(0.445, 1), 0.5)
|
|
54
|
+
|
|
55
|
+
# -------------------- Продвинутые тесты --------------------
|
|
56
|
+
|
|
57
|
+
def test_float_representation_fix(self):
|
|
58
|
+
"""
|
|
59
|
+
Тест на 'проблему 2.675'.
|
|
60
|
+
Стандартный round(2.675, 2) дает 2.67.
|
|
61
|
+
Математическое округление должно давать 2.68.
|
|
62
|
+
"""
|
|
63
|
+
self.assertEqual(true_round(2.675, 2), 2.68)
|
|
64
|
+
self.assertEqual(true_round(1.005, 2), 1.01)
|
|
65
|
+
|
|
66
|
+
def test_heavy_cascade(self):
|
|
67
|
+
"""
|
|
68
|
+
Тест 'Эффект Домино'.
|
|
69
|
+
9.999 при округлении до 2 знаков должно стать 10.0
|
|
70
|
+
"""
|
|
71
|
+
# 9.999 -> 9.9(9+1) -> 9.(9+1)0 -> (9+1).00 -> 10.0
|
|
72
|
+
self.assertEqual(true_round(9.999, 2), 10.0)
|
|
73
|
+
|
|
74
|
+
# 89.999 -> 90.0
|
|
75
|
+
self.assertEqual(true_round(89.999, 2), 90.0)
|
|
76
|
+
|
|
77
|
+
def test_small_numbers(self):
|
|
78
|
+
"""Работа с очень маленькими числами."""
|
|
79
|
+
# 0.0005 -> округляем до 3 знаков -> 0.001
|
|
80
|
+
self.assertEqual(true_round(0.0005, 3), 0.001)
|
|
81
|
+
|
|
82
|
+
# 0.0004 -> округляем до 3 знаков -> 0.0
|
|
83
|
+
self.assertEqual(true_round(0.0004, 3), 0.0)
|
|
84
|
+
|
|
85
|
+
def test_rounding_to_zero_length(self):
|
|
86
|
+
"""
|
|
87
|
+
Явное указание length=0.
|
|
88
|
+
Должно работать так же, как без указания length.
|
|
89
|
+
"""
|
|
90
|
+
self.assertEqual(true_round(5.6, 0), 6)
|
|
91
|
+
self.assertIsInstance(true_round(5.6, 0), int)
|
|
92
|
+
|
|
93
|
+
def test_negative_numbers(self):
|
|
94
|
+
"""
|
|
95
|
+
Отрицательные числа.
|
|
96
|
+
Математически: -1.5 округляется до -2 (по модулю).
|
|
97
|
+
"""
|
|
98
|
+
self.assertEqual(true_round(-1.5), -2)
|
|
99
|
+
self.assertEqual(true_round(-1.1), -1)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
# Запуск конкретного теста:
|
|
104
|
+
# unittest.main(defaultTest='TestTrueRound.test_negative_numbers')
|
|
105
|
+
|
|
106
|
+
# Запуск всех тестов:
|
|
107
|
+
unittest.main()
|