-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnumber_factory.py
153 lines (135 loc) · 5.57 KB
/
number_factory.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import math
import random
import time
from number_helper import number_is_zero, number_is_equal, number_fix
class RandomGenerator:
def __init__(self, seed: int = None):
self._seed = seed
self._random = random.Random(seed)
def next_int(self, maximum: int) -> int:
return self._random.randint(0, maximum)
class NumberFactory:
def __init__(
self,
minimum: float = 1.0,
maximum: float = 10.0,
step: float = 1,
random_generator: RandomGenerator | None = None,
):
self._number_stat: dict[str, int] = {}
self._min: float = minimum
self._max: float = maximum
if step <= 0:
raise ValueError("Step must be greater than 0")
self._step = step
self._decimals = NumberFactory._get_decimals_from_step(step)
self._random_generator: RandomGenerator = random_generator or RandomGenerator()
@staticmethod
def _get_decimals_from_step(step: float) -> int:
str_step = ("%.10f" % step).rstrip("0")
if "." in str_step:
return len(str_step.split(".")[1])
return 0
def get_step(self) -> float:
return self._step
def get_minimum(self) -> float:
return self._min
def get_maximum(self) -> float:
return self._max
def get_decimals(self) -> int:
return self._decimals
def next(
self,
minimum: float | None = None,
maximum: float | None = None,
dividable_by: float | None = None,
zero_allowed: bool = True,
) -> float:
minimum = max(self._min, minimum or self._min)
maximum = min(self._max, maximum or self._max)
if minimum > maximum:
raise ValueError(f"Minimum is greater than maximum: {minimum} > {maximum}")
if dividable_by is not None:
if number_is_zero(dividable_by):
dividable_by = self._step
else:
dividable_by = abs(dividable_by)
a = int(dividable_by * 10**self._decimals)
b = int(self._step * 10**self._decimals)
if a % b != 0:
raise ValueError(
f"Dividable by must be dividable by step: {dividable_by} vs {self._step}"
)
else:
dividable_by = self._step
minimum_start = math.ceil(minimum / dividable_by) * dividable_by
maximum_end = math.floor(maximum / dividable_by) * dividable_by
full_range = abs(maximum_end - minimum_start)
range_int = int(full_range / dividable_by)
start_time = time.time()
if (
not zero_allowed
and dividable_by > abs(minimum_start)
and dividable_by > abs(maximum_end)
):
raise ValueError(
f"Dividable by / step must be less than minimum or maximum: {dividable_by} vs [{minimum_start}, {maximum_end}]"
)
if minimum_start > maximum_end:
raise ValueError(
f"Minimum is greater than maximum (modified): {minimum_start} > {maximum_end}"
)
max_runtime_sec = 0.1
while time.time() - start_time < max_runtime_sec:
random_in_range = self._random_generator.next_int(range_int)
value = minimum_start + random_in_range * dividable_by
value_fixed = self.fix(value, dividable_by)
if not number_is_equal(value, self.fix(value, dividable_by)):
raise RuntimeError(
f"Value is not equal to fixed value: {value} vs {value_fixed}"
)
if number_fix(value) < number_fix(minimum_start):
raise RuntimeError(
f"Value is less than minimum: {value} < {minimum_start} minimum={minimum}"
)
if number_fix(value) > number_fix(maximum_end):
raise RuntimeError(
f"Value is greater than maximum: {value} > {maximum_end} maximum={maximum}"
)
if not zero_allowed and number_is_zero(value):
continue
return self.fix(value)
raise RuntimeError(
f"Cannot find random value in {max_runtime_sec} second, paramters: minimum={minimum}, maximum={maximum}, dividable_by={dividable_by}, zero_allowed={zero_allowed}"
)
def format(self, value: float | None, decimals: int | None = None) -> str:
if value is None:
return ""
if decimals is None:
decimals = self._decimals
return f"{value:.{0 if decimals < 1 else decimals}f}"
def fix(self, value: float, step: float | None = None) -> float:
if step is None:
step = self._step
elif step < self._step:
raise ValueError(
f"Step must be greater than or equal to the factory step: {self._step} vs {step}"
)
return float(round(round(value / step) * step, self._decimals))
def fly_back(self, value: float):
str_value = self.format(value)
if str_value not in self._number_stat:
self._number_stat[str_value] = 0
self._number_stat[str_value] += 1
def print_statistic(self):
swapped_stat = {}
for k, v in self._number_stat.items():
if v not in swapped_stat:
swapped_stat[v] = []
swapped_stat[v].append(float(k))
print("Number factory statistic:")
for k in sorted(swapped_stat.keys(), reverse=True):
v = swapped_stat[k]
v.sort()
joined_v = ", ".join(map(str, v))
print(f"{k:<5} -> {joined_v}")