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.
@@ -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
+ [![Python](https://img.shields.io/badge/Made%20with-Python-blue.svg)](https://www.python.org/)
19
+ [![Last Commit](https://img.shields.io/github/last-commit/n1xsi/pycustomrand.svg)](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
+ [![Python](https://img.shields.io/badge/Made%20with-Python-blue.svg)](https://www.python.org/)
4
+ [![Last Commit](https://img.shields.io/github/last-commit/n1xsi/pycustomrand.svg)](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
+ [![Python](https://img.shields.io/badge/Made%20with-Python-blue.svg)](https://www.python.org/)
19
+ [![Last Commit](https://img.shields.io/github/last-commit/n1xsi/pycustomrand.svg)](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
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()