libdev 0.92__tar.gz → 0.94__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.
- {libdev-0.92 → libdev-0.94}/PKG-INFO +1 -1
- {libdev-0.92 → libdev-0.94}/libdev/__init__.py +1 -1
- libdev-0.94/libdev/num.py +437 -0
- {libdev-0.92 → libdev-0.94}/libdev/time.py +21 -0
- {libdev-0.92 → libdev-0.94}/libdev.egg-info/PKG-INFO +1 -1
- {libdev-0.92 → libdev-0.94}/tests/test_num.py +92 -0
- libdev-0.92/libdev/num.py +0 -244
- {libdev-0.92 → libdev-0.94}/LICENSE +0 -0
- {libdev-0.92 → libdev-0.94}/README.md +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/cfg.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/check.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/codes.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/crypt.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/dev.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/doc.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/fin.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/gen.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/img.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/lang.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/log.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/req.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev/s3.py +0 -0
- {libdev-0.92 → libdev-0.94}/libdev.egg-info/SOURCES.txt +0 -0
- {libdev-0.92 → libdev-0.94}/libdev.egg-info/dependency_links.txt +0 -0
- {libdev-0.92 → libdev-0.94}/libdev.egg-info/requires.txt +0 -0
- {libdev-0.92 → libdev-0.94}/libdev.egg-info/top_level.txt +0 -0
- {libdev-0.92 → libdev-0.94}/setup.cfg +0 -0
- {libdev-0.92 → libdev-0.94}/setup.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_cfg.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_check.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_codes.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_crypt.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_dev.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_doc.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_gen.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_img.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_lang.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_log.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_req.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_s3.py +0 -0
- {libdev-0.92 → libdev-0.94}/tests/test_time.py +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numbers functionality
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import math
|
|
7
|
+
from decimal import Decimal, InvalidOperation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_SUBSCRIPTS = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_float(value: str) -> bool:
|
|
14
|
+
"""Check value for float"""
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
float(value)
|
|
18
|
+
except (ValueError, TypeError):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def to_num(value) -> bool:
|
|
25
|
+
"""Convert value to int or float"""
|
|
26
|
+
|
|
27
|
+
if value is None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
value = float(value.strip())
|
|
32
|
+
|
|
33
|
+
if not value % 1:
|
|
34
|
+
value = int(value)
|
|
35
|
+
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_int(value) -> int:
|
|
40
|
+
"""Choose only decimal"""
|
|
41
|
+
|
|
42
|
+
if not value:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
return int(re.sub(r"\D", "", str(value)))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_float(value) -> list:
|
|
49
|
+
"""Get a list of floats"""
|
|
50
|
+
|
|
51
|
+
if value is None:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
numbers = re.findall(r"[-+]?\d*\.\d+|[-+]?\d+", value)
|
|
55
|
+
return [float(number) for number in numbers]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_decimals(value):
|
|
59
|
+
"""Get count of decimal"""
|
|
60
|
+
|
|
61
|
+
if isinstance(value, str):
|
|
62
|
+
while value[-1] == "0":
|
|
63
|
+
value = value[:-1]
|
|
64
|
+
|
|
65
|
+
return abs(Decimal(str(value)).as_tuple().exponent)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_whole(value):
|
|
69
|
+
"""Get whole view of a number"""
|
|
70
|
+
|
|
71
|
+
if isinstance(value, int) or (isinstance(value, str) and "." not in value):
|
|
72
|
+
# NOTE: to remove 0 in the start of the string
|
|
73
|
+
return str(int(value))
|
|
74
|
+
|
|
75
|
+
# NOTE: float for add . to int & support str
|
|
76
|
+
value = float(value)
|
|
77
|
+
|
|
78
|
+
# NOTE: to avoid the exponential form of the number
|
|
79
|
+
return f"{value:.{find_decimals(value)}f}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def simplify_value(value, decimals=4):
|
|
83
|
+
"""Get the significant part of a number"""
|
|
84
|
+
|
|
85
|
+
if value is None:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
value = get_whole(value)
|
|
89
|
+
if "." not in value:
|
|
90
|
+
value += "."
|
|
91
|
+
|
|
92
|
+
whole, fractional = value.split(".")
|
|
93
|
+
|
|
94
|
+
if value[0] == "-":
|
|
95
|
+
sign = "-"
|
|
96
|
+
whole = whole[1:]
|
|
97
|
+
else:
|
|
98
|
+
sign = ""
|
|
99
|
+
|
|
100
|
+
if whole != "0":
|
|
101
|
+
digit = len(whole)
|
|
102
|
+
value = whole + "." + fractional[: max(0, decimals - digit)]
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
offset = 0
|
|
106
|
+
while fractional and fractional[0] == "0":
|
|
107
|
+
offset += 1
|
|
108
|
+
fractional = fractional[1:]
|
|
109
|
+
|
|
110
|
+
value = "0." + "0" * offset + fractional[:decimals]
|
|
111
|
+
|
|
112
|
+
while value[-1] == "0":
|
|
113
|
+
value = value[:-1]
|
|
114
|
+
|
|
115
|
+
if value[-1] == ".":
|
|
116
|
+
value = value[:-1]
|
|
117
|
+
|
|
118
|
+
return sign + value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def pretty(
|
|
122
|
+
value,
|
|
123
|
+
decimals=None,
|
|
124
|
+
sign=False,
|
|
125
|
+
symbol="’",
|
|
126
|
+
zeros=4,
|
|
127
|
+
compress=None,
|
|
128
|
+
):
|
|
129
|
+
"""Decorate the number beautifully"""
|
|
130
|
+
|
|
131
|
+
if value is None:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# Handle decimals parameter first (takes precedence)
|
|
135
|
+
if decimals is not None:
|
|
136
|
+
# Use the original decimals logic for backward compatibility
|
|
137
|
+
s = to_plain(abs(value))
|
|
138
|
+
if "." in s:
|
|
139
|
+
int_part = s.split(".", 1)[0]
|
|
140
|
+
cur = len(int_part)
|
|
141
|
+
target_decimals = max(0, decimals - cur)
|
|
142
|
+
# Apply rounding first
|
|
143
|
+
rounded_value = round(float(value), target_decimals)
|
|
144
|
+
# If target_decimals is 0, convert to int for proper formatting
|
|
145
|
+
if target_decimals == 0:
|
|
146
|
+
rounded_value = int(rounded_value)
|
|
147
|
+
# Then use compress_zeros without round parameter
|
|
148
|
+
data = compress_zeros(rounded_value, zeros=zeros)
|
|
149
|
+
else:
|
|
150
|
+
data = compress_zeros(value, zeros=zeros)
|
|
151
|
+
elif zeros is None and compress is None:
|
|
152
|
+
# No compression or special formatting requested, use plain representation
|
|
153
|
+
data = to_plain(value)
|
|
154
|
+
else:
|
|
155
|
+
# Use compress_zeros with specified parameters
|
|
156
|
+
compress_zeros_args = {}
|
|
157
|
+
|
|
158
|
+
if zeros is not None:
|
|
159
|
+
compress_zeros_args["zeros"] = zeros
|
|
160
|
+
|
|
161
|
+
if compress is not None:
|
|
162
|
+
compress_zeros_args["round"] = compress
|
|
163
|
+
|
|
164
|
+
data = compress_zeros(value, **compress_zeros_args)
|
|
165
|
+
|
|
166
|
+
if data == "0":
|
|
167
|
+
return "0"
|
|
168
|
+
|
|
169
|
+
# Remove trailing zeros after decimal point for cleaner formatting
|
|
170
|
+
if "." in data and data.rsplit(".", maxsplit=1)[-1] == "0":
|
|
171
|
+
data = data.split(".", maxsplit=1)[0]
|
|
172
|
+
|
|
173
|
+
if symbol:
|
|
174
|
+
data = add_radix(data, symbol)
|
|
175
|
+
|
|
176
|
+
if sign:
|
|
177
|
+
if data[0] != "-":
|
|
178
|
+
data = "+" + data
|
|
179
|
+
|
|
180
|
+
return data
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def add_sign(value):
|
|
184
|
+
"""Add sign to a number"""
|
|
185
|
+
|
|
186
|
+
if value is None:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
sign = ""
|
|
190
|
+
|
|
191
|
+
if float(value) > 0:
|
|
192
|
+
sign = "+"
|
|
193
|
+
elif value == 0:
|
|
194
|
+
value = abs(value)
|
|
195
|
+
|
|
196
|
+
return f"{sign}{get_whole(value)}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def add_radix(value, symbol="’"):
|
|
200
|
+
"""Add radix to a number"""
|
|
201
|
+
|
|
202
|
+
if value is None:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
value = str(value)
|
|
206
|
+
|
|
207
|
+
if "." in value:
|
|
208
|
+
integer, fractional = value.split(".")
|
|
209
|
+
else:
|
|
210
|
+
integer = value
|
|
211
|
+
fractional = ""
|
|
212
|
+
|
|
213
|
+
if integer[0] == "-":
|
|
214
|
+
sign = "-"
|
|
215
|
+
integer = integer[1:]
|
|
216
|
+
# elif integer[0] == '+':
|
|
217
|
+
# sign = '+'
|
|
218
|
+
# integer = integer[1:]
|
|
219
|
+
else:
|
|
220
|
+
sign = ""
|
|
221
|
+
|
|
222
|
+
data = ""
|
|
223
|
+
ind = 0
|
|
224
|
+
for i in integer[::-1]:
|
|
225
|
+
if ind and ind % 3 == 0:
|
|
226
|
+
data = symbol + data
|
|
227
|
+
ind += 1
|
|
228
|
+
data = i + data
|
|
229
|
+
|
|
230
|
+
data = sign + data
|
|
231
|
+
if fractional:
|
|
232
|
+
data += "." + fractional
|
|
233
|
+
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def mul(x, y):
|
|
238
|
+
"""Multiply fractions correctly"""
|
|
239
|
+
if x is None or y is None:
|
|
240
|
+
return None
|
|
241
|
+
return float(Decimal(str(x)) * Decimal(str(y)))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def div(x, y):
|
|
245
|
+
"""Divide fractions correctly"""
|
|
246
|
+
if x is None or y is None:
|
|
247
|
+
return None
|
|
248
|
+
return float(Decimal(str(x)) / Decimal(str(y)))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def add(x, y):
|
|
252
|
+
"""Subtract fractions correctly"""
|
|
253
|
+
if x is None or y is None:
|
|
254
|
+
return None
|
|
255
|
+
return float(Decimal(str(x)) + Decimal(str(y)))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def sub(x, y):
|
|
259
|
+
"""Subtract fractions correctly"""
|
|
260
|
+
if x is None or y is None:
|
|
261
|
+
return None
|
|
262
|
+
return float(Decimal(str(x)) - Decimal(str(y)))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def to_step(value, step=1, side=False):
|
|
266
|
+
"""Change value step"""
|
|
267
|
+
|
|
268
|
+
if value is None:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
value = div(value, step)
|
|
272
|
+
if side:
|
|
273
|
+
value = math.ceil(value)
|
|
274
|
+
else:
|
|
275
|
+
value = math.floor(value)
|
|
276
|
+
value = mul(value, step)
|
|
277
|
+
|
|
278
|
+
if step >= 1:
|
|
279
|
+
value = int(value)
|
|
280
|
+
|
|
281
|
+
return value
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def to_plain(value) -> str:
|
|
285
|
+
if value is None:
|
|
286
|
+
return None
|
|
287
|
+
try:
|
|
288
|
+
if isinstance(value, str):
|
|
289
|
+
d = Decimal(value)
|
|
290
|
+
elif isinstance(value, float):
|
|
291
|
+
d = Decimal(str(value))
|
|
292
|
+
else:
|
|
293
|
+
d = Decimal(value)
|
|
294
|
+
|
|
295
|
+
s = format(d.normalize(), "f")
|
|
296
|
+
|
|
297
|
+
if "." in s:
|
|
298
|
+
s = s.rstrip("0").rstrip(".")
|
|
299
|
+
if s == "-0":
|
|
300
|
+
s = "0"
|
|
301
|
+
return s
|
|
302
|
+
except (InvalidOperation, ValueError, TypeError):
|
|
303
|
+
return str(value)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _round_to_decimals(x, decimals):
|
|
307
|
+
"""Helper function to round to specified decimal places"""
|
|
308
|
+
if decimals <= 0:
|
|
309
|
+
return float(int(x))
|
|
310
|
+
return round(float(x), decimals)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def compress_zeros(x, zeros=2, round=None) -> str:
|
|
314
|
+
"""
|
|
315
|
+
0.000012 -> '0.0₄12'
|
|
316
|
+
1.000045 -> '1.0₄45'
|
|
317
|
+
round: number of digits after the zero block (rounds).
|
|
318
|
+
zeros: minimum count of consecutive zeros to compress (default: 2).
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
if x is None:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
# Store original string representation for rounding calculations
|
|
325
|
+
original_str = None
|
|
326
|
+
if isinstance(x, str):
|
|
327
|
+
original_str = x.strip()
|
|
328
|
+
x = original_str
|
|
329
|
+
# Remove trailing zeros from string
|
|
330
|
+
if "." in x:
|
|
331
|
+
x = x.rstrip("0").rstrip(".")
|
|
332
|
+
# Convert to appropriate numeric type
|
|
333
|
+
try:
|
|
334
|
+
if "." in x:
|
|
335
|
+
x = float(x)
|
|
336
|
+
else:
|
|
337
|
+
x = int(x)
|
|
338
|
+
except ValueError:
|
|
339
|
+
return str(x)
|
|
340
|
+
|
|
341
|
+
# Determine if original was float or int to preserve format
|
|
342
|
+
is_float_type = isinstance(x, float) or (isinstance(x, str) and "." in str(x))
|
|
343
|
+
|
|
344
|
+
# Handle rounding if specified
|
|
345
|
+
if round is not None:
|
|
346
|
+
# For rounding, use original string if available, otherwise convert to plain string
|
|
347
|
+
if original_str and "." in original_str:
|
|
348
|
+
s = original_str.lstrip("-")
|
|
349
|
+
else:
|
|
350
|
+
# Use to_plain to avoid scientific notation
|
|
351
|
+
s = to_plain(abs(x))
|
|
352
|
+
|
|
353
|
+
if "." in s:
|
|
354
|
+
int_part, frac_part = s.split(".")
|
|
355
|
+
# Count leading zeros in fractional part
|
|
356
|
+
leading_zeros = 0
|
|
357
|
+
for c in frac_part:
|
|
358
|
+
if c == "0":
|
|
359
|
+
leading_zeros += 1
|
|
360
|
+
else:
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
# Count trailing zeros in fractional part
|
|
364
|
+
trailing_zeros = 0
|
|
365
|
+
for c in reversed(frac_part):
|
|
366
|
+
if c == "0":
|
|
367
|
+
trailing_zeros += 1
|
|
368
|
+
else:
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
# Apply rounding logic
|
|
372
|
+
if leading_zeros > 0:
|
|
373
|
+
# If there are leading zeros, round after them (regardless of compression)
|
|
374
|
+
total_decimals = leading_zeros + round
|
|
375
|
+
x = _round_to_decimals(x, total_decimals)
|
|
376
|
+
else:
|
|
377
|
+
# No leading zeros, apply normal rounding
|
|
378
|
+
x = _round_to_decimals(x, round)
|
|
379
|
+
|
|
380
|
+
# Convert to string representation
|
|
381
|
+
if isinstance(x, int) and not is_float_type:
|
|
382
|
+
s = str(x)
|
|
383
|
+
else:
|
|
384
|
+
# For floats, use format that preserves trailing decimals when needed
|
|
385
|
+
if x == int(x) and is_float_type:
|
|
386
|
+
s = f"{int(x)}.0"
|
|
387
|
+
else:
|
|
388
|
+
s = str(float(x))
|
|
389
|
+
# Remove scientific notation if present
|
|
390
|
+
if "e" in s.lower():
|
|
391
|
+
s = f"{float(x):.15f}".rstrip("0")
|
|
392
|
+
if s.endswith("."):
|
|
393
|
+
s += "0"
|
|
394
|
+
|
|
395
|
+
# Handle negative sign
|
|
396
|
+
negative = s.startswith("-")
|
|
397
|
+
if negative:
|
|
398
|
+
s = s[1:]
|
|
399
|
+
|
|
400
|
+
# Process compression
|
|
401
|
+
if "." not in s:
|
|
402
|
+
result = s
|
|
403
|
+
else:
|
|
404
|
+
int_part, frac_part = s.split(".")
|
|
405
|
+
|
|
406
|
+
# Compress leading zeros in fractional part
|
|
407
|
+
leading_zeros = 0
|
|
408
|
+
for c in frac_part:
|
|
409
|
+
if c == "0":
|
|
410
|
+
leading_zeros += 1
|
|
411
|
+
else:
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
# Compress trailing zeros in fractional part
|
|
415
|
+
trailing_zeros = 0
|
|
416
|
+
for c in reversed(frac_part):
|
|
417
|
+
if c == "0":
|
|
418
|
+
trailing_zeros += 1
|
|
419
|
+
else:
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
# Apply compression
|
|
423
|
+
if leading_zeros >= zeros:
|
|
424
|
+
# Compress leading zeros
|
|
425
|
+
remaining_frac = frac_part[leading_zeros:]
|
|
426
|
+
result = f"{int_part}.0{str(leading_zeros).translate(_SUBSCRIPTS)}{remaining_frac}"
|
|
427
|
+
elif trailing_zeros >= zeros and leading_zeros == 0:
|
|
428
|
+
# Compress trailing zeros (but not if there are leading zeros)
|
|
429
|
+
remaining_frac = frac_part[:-trailing_zeros]
|
|
430
|
+
if remaining_frac:
|
|
431
|
+
result = f"{int_part}.{remaining_frac}0{str(trailing_zeros).translate(_SUBSCRIPTS)}"
|
|
432
|
+
else:
|
|
433
|
+
result = f"{int_part}.0{str(trailing_zeros).translate(_SUBSCRIPTS)}"
|
|
434
|
+
else:
|
|
435
|
+
result = s
|
|
436
|
+
|
|
437
|
+
return f"-{result}" if negative else result
|
|
@@ -300,6 +300,27 @@ def get_month_start(timestamp=None, tz=0):
|
|
|
300
300
|
return int(start_month.timestamp())
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
def get_week_start(timestamp=None, tz=0):
|
|
304
|
+
"""
|
|
305
|
+
Get the start of the week (midnight on Monday) for a given timestamp in a specified timezone.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
timestamp (float): The original timestamp (in seconds since epoch). Defaults to the current time if None.
|
|
309
|
+
tz (int): The timezone offset in hours (e.g., 3 for UTC+3).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
float: The timestamp for the start of the week (Monday at midnight) in the specified timezone.
|
|
313
|
+
"""
|
|
314
|
+
if timestamp is None:
|
|
315
|
+
timestamp = time.time()
|
|
316
|
+
dt_local = datetime.datetime.fromtimestamp(timestamp, tz=to_tz(tz))
|
|
317
|
+
# Calculate days to subtract to get to Monday (weekday() returns 0 for Monday, 6 for Sunday)
|
|
318
|
+
days_since_monday = dt_local.weekday()
|
|
319
|
+
start_week = dt_local - datetime.timedelta(days=days_since_monday)
|
|
320
|
+
start_week = start_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
321
|
+
return int(start_week.timestamp())
|
|
322
|
+
|
|
323
|
+
|
|
303
324
|
def get_next_day(timestamp=None, tz=0):
|
|
304
325
|
"""
|
|
305
326
|
Get the start of the next day (midnight) for a given timestamp in a specified timezone.
|
|
@@ -11,6 +11,8 @@ from libdev.num import (
|
|
|
11
11
|
to_step,
|
|
12
12
|
add,
|
|
13
13
|
pretty,
|
|
14
|
+
to_plain,
|
|
15
|
+
compress_zeros,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -155,3 +157,93 @@ def test_pretty():
|
|
|
155
157
|
assert pretty(123.456, 1) == "123"
|
|
156
158
|
assert pretty(123.456, 1, True) == "+123"
|
|
157
159
|
assert pretty(12345.6, 3, True) == "+12’346"
|
|
160
|
+
assert pretty(-0.000000235235, zeros=None, compress=None) == "-0.000000235235"
|
|
161
|
+
assert pretty(-0.000000235235, zeros=4, compress=2) == "-0.0₆24"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_to_plain():
|
|
165
|
+
assert to_plain(None) == None
|
|
166
|
+
assert to_plain(0) == "0"
|
|
167
|
+
# assert to_plain(0.0) == "0.0"
|
|
168
|
+
# assert to_plain(1.0) == "1.0"
|
|
169
|
+
assert to_plain(1.1) == "1.1"
|
|
170
|
+
assert to_plain(0.000000235235) == "0.000000235235"
|
|
171
|
+
assert to_plain(-0.000000235235) == "-0.000000235235"
|
|
172
|
+
assert to_plain("0.000000235235") == "0.000000235235"
|
|
173
|
+
assert to_plain(2.35235e-07) == "0.000000235235"
|
|
174
|
+
assert to_plain("1e-12") == "0.000000000001"
|
|
175
|
+
assert to_plain("123.4500") == "123.45"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_pretty_with_compression():
|
|
179
|
+
# Test pretty function with compression parameters
|
|
180
|
+
|
|
181
|
+
# Test zeros parameter (minimum zeros to compress)
|
|
182
|
+
assert pretty(0.00012, zeros=2) == "0.0₃12" # Default behavior, compress >=2 zeros
|
|
183
|
+
assert pretty(0.01, zeros=1) == "0.0₁1" # Compress single zero with zeros=1
|
|
184
|
+
assert pretty(0.01, zeros=3) == "0.01" # Don't compress with zeros=3 (only 1 zero)
|
|
185
|
+
assert pretty(0.0001, zeros=3) == "0.0₃1" # Compress with zeros=3 (3 zeros)
|
|
186
|
+
|
|
187
|
+
# Test compress parameter (round after zero block)
|
|
188
|
+
assert (
|
|
189
|
+
pretty(0.00012345, zeros=2, compress=2) == "0.0₃12"
|
|
190
|
+
) # Round to 2 digits after zeros
|
|
191
|
+
assert (
|
|
192
|
+
pretty(0.00012345, zeros=4, compress=4) == "0.0001234"
|
|
193
|
+
) # No compression (3 < 4 zeros), 4 digits after 3 zeros
|
|
194
|
+
assert pretty(0.00012345, zeros=4, compress=8) == "0.00012345"
|
|
195
|
+
assert (
|
|
196
|
+
pretty(0.00012345, zeros=2, compress=3) == "0.0₃123"
|
|
197
|
+
) # Round to 3 digits after zeros
|
|
198
|
+
assert (
|
|
199
|
+
pretty(-0.0010959999999999997522, compress=3) == "-0.0011"
|
|
200
|
+
) # Negative with rounding
|
|
201
|
+
|
|
202
|
+
# Test both parameters together
|
|
203
|
+
assert (
|
|
204
|
+
pretty("0.0000012345", zeros=4, compress=2) == "0.0₅12"
|
|
205
|
+
) # 5 zeros, round to 2 digits
|
|
206
|
+
assert (
|
|
207
|
+
pretty("0.00012345", zeros=4, compress=2) == "0.00012"
|
|
208
|
+
) # Only 3 zeros, no compression, 2 digits after 3 zeros
|
|
209
|
+
|
|
210
|
+
# Test that other pretty parameters still work with compression
|
|
211
|
+
assert pretty(0.00012, zeros=2, sign=True) == "+0.0₃12" # With sign
|
|
212
|
+
assert pretty(12000.00012, zeros=2, symbol="'") == "12'000.0₃12" # With radix
|
|
213
|
+
assert (
|
|
214
|
+
pretty(-0.00012, zeros=2, sign=True, symbol="'") == "-0.0₃12"
|
|
215
|
+
) # Negative with sign and radix
|
|
216
|
+
|
|
217
|
+
# Test edge cases
|
|
218
|
+
assert pretty(0, zeros=1) == "0" # Zero value
|
|
219
|
+
assert pretty(None, zeros=2) == None # None value
|
|
220
|
+
assert (
|
|
221
|
+
pretty(1.0, zeros=2) == "1"
|
|
222
|
+
) # No fractional zeros to compress, trailing zero removed for clean formatting
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_compress_zeros():
|
|
226
|
+
assert compress_zeros(None) == None
|
|
227
|
+
assert compress_zeros(0) == "0"
|
|
228
|
+
assert compress_zeros(0.0) == "0.0"
|
|
229
|
+
assert compress_zeros(1) == "1"
|
|
230
|
+
assert compress_zeros(1.0) == "1.0"
|
|
231
|
+
assert compress_zeros(1.0, round=0) == "1.0"
|
|
232
|
+
assert compress_zeros(0.00012) == "0.0₃12"
|
|
233
|
+
assert compress_zeros(0.00012, round=2) == "0.0₃12"
|
|
234
|
+
assert compress_zeros(0.0123) == "0.0123"
|
|
235
|
+
assert compress_zeros(0.0123456, round=3) == "0.0123"
|
|
236
|
+
assert compress_zeros(1.000045) == "1.0₄45"
|
|
237
|
+
assert compress_zeros(0.0010859999999999997522, round=3) == "0.0₂109"
|
|
238
|
+
assert compress_zeros(-0.0010959999999999997522, round=3) == "-0.0₂11"
|
|
239
|
+
assert compress_zeros("-0.012300") == "-0.0123"
|
|
240
|
+
assert compress_zeros(0.01, zeros=1) == "0.0₁1"
|
|
241
|
+
assert compress_zeros(0.001, zeros=1) == "0.0₂1"
|
|
242
|
+
assert compress_zeros("1.10", zeros=1) == "1.1"
|
|
243
|
+
assert compress_zeros(0.01, zeros=3) == "0.01"
|
|
244
|
+
assert compress_zeros(0.001, zeros=3) == "0.001"
|
|
245
|
+
assert compress_zeros(0.0001, zeros=3) == "0.0₃1"
|
|
246
|
+
assert compress_zeros(0.00001, zeros=3) == "0.0₄1"
|
|
247
|
+
assert compress_zeros(0.0000012, zeros=1) == "0.0₅12"
|
|
248
|
+
assert compress_zeros(0.0000012, zeros=5) == "0.0₅12"
|
|
249
|
+
assert compress_zeros(0.0000012, zeros=6) == "0.0000012"
|
libdev-0.92/libdev/num.py
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Numbers functionality
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import math
|
|
7
|
-
from decimal import Decimal
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def is_float(value: str) -> bool:
|
|
11
|
-
"""Check value for float"""
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
float(value)
|
|
15
|
-
except (ValueError, TypeError):
|
|
16
|
-
return False
|
|
17
|
-
|
|
18
|
-
return True
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def to_num(value) -> bool:
|
|
22
|
-
"""Convert value to int or float"""
|
|
23
|
-
|
|
24
|
-
if value is None:
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
if isinstance(value, str):
|
|
28
|
-
value = float(value.strip())
|
|
29
|
-
|
|
30
|
-
if not value % 1:
|
|
31
|
-
value = int(value)
|
|
32
|
-
|
|
33
|
-
return value
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def to_int(value) -> int:
|
|
37
|
-
"""Choose only decimal"""
|
|
38
|
-
|
|
39
|
-
if not value:
|
|
40
|
-
return 0
|
|
41
|
-
|
|
42
|
-
return int(re.sub(r"\D", "", str(value)))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_float(value) -> list:
|
|
46
|
-
"""Get a list of floats"""
|
|
47
|
-
|
|
48
|
-
if value is None:
|
|
49
|
-
return []
|
|
50
|
-
|
|
51
|
-
numbers = re.findall(r"[-+]?\d*\.\d+|[-+]?\d+", value)
|
|
52
|
-
return [float(number) for number in numbers]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def find_decimals(value):
|
|
56
|
-
"""Get count of decimal"""
|
|
57
|
-
|
|
58
|
-
if isinstance(value, str):
|
|
59
|
-
while value[-1] == "0":
|
|
60
|
-
value = value[:-1]
|
|
61
|
-
|
|
62
|
-
return abs(Decimal(str(value)).as_tuple().exponent)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def get_whole(value):
|
|
66
|
-
"""Get whole view of a number"""
|
|
67
|
-
|
|
68
|
-
if isinstance(value, int) or (isinstance(value, str) and "." not in value):
|
|
69
|
-
# NOTE: to remove 0 in the start of the string
|
|
70
|
-
return str(int(value))
|
|
71
|
-
|
|
72
|
-
# NOTE: float for add . to int & support str
|
|
73
|
-
value = float(value)
|
|
74
|
-
|
|
75
|
-
# NOTE: to avoid the exponential form of the number
|
|
76
|
-
return f"{value:.{find_decimals(value)}f}"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def simplify_value(value, decimals=4):
|
|
80
|
-
"""Get the significant part of a number"""
|
|
81
|
-
|
|
82
|
-
if value is None:
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
value = get_whole(value)
|
|
86
|
-
if "." not in value:
|
|
87
|
-
value += "."
|
|
88
|
-
|
|
89
|
-
whole, fractional = value.split(".")
|
|
90
|
-
|
|
91
|
-
if value[0] == "-":
|
|
92
|
-
sign = "-"
|
|
93
|
-
whole = whole[1:]
|
|
94
|
-
else:
|
|
95
|
-
sign = ""
|
|
96
|
-
|
|
97
|
-
if whole != "0":
|
|
98
|
-
digit = len(whole)
|
|
99
|
-
value = whole + "." + fractional[: max(0, decimals - digit)]
|
|
100
|
-
|
|
101
|
-
else:
|
|
102
|
-
offset = 0
|
|
103
|
-
while fractional and fractional[0] == "0":
|
|
104
|
-
offset += 1
|
|
105
|
-
fractional = fractional[1:]
|
|
106
|
-
|
|
107
|
-
value = "0." + "0" * offset + fractional[:decimals]
|
|
108
|
-
|
|
109
|
-
while value[-1] == "0":
|
|
110
|
-
value = value[:-1]
|
|
111
|
-
|
|
112
|
-
if value[-1] == ".":
|
|
113
|
-
value = value[:-1]
|
|
114
|
-
|
|
115
|
-
return sign + value
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def pretty(value, decimals=None, sign=False, symbol="’"):
|
|
119
|
-
"""Decorate the number beautifully"""
|
|
120
|
-
|
|
121
|
-
if value is None:
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
data = str(float(value))
|
|
125
|
-
|
|
126
|
-
if decimals is not None:
|
|
127
|
-
cur = len(data.split(".", maxsplit=1)[0])
|
|
128
|
-
data = str(round(value, max(0, decimals - cur)))
|
|
129
|
-
|
|
130
|
-
if data.rsplit(".", maxsplit=1)[-1] == "0":
|
|
131
|
-
data = data.split(".", maxsplit=1)[0]
|
|
132
|
-
|
|
133
|
-
if data == "0":
|
|
134
|
-
return "0"
|
|
135
|
-
|
|
136
|
-
if symbol:
|
|
137
|
-
data = add_radix(data, symbol)
|
|
138
|
-
|
|
139
|
-
if sign:
|
|
140
|
-
if data[0] != "-":
|
|
141
|
-
data = "+" + data
|
|
142
|
-
|
|
143
|
-
return data
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def add_sign(value):
|
|
147
|
-
"""Add sign to a number"""
|
|
148
|
-
|
|
149
|
-
if value is None:
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
sign = ""
|
|
153
|
-
|
|
154
|
-
if float(value) > 0:
|
|
155
|
-
sign = "+"
|
|
156
|
-
elif value == 0:
|
|
157
|
-
value = abs(value)
|
|
158
|
-
|
|
159
|
-
return f"{sign}{get_whole(value)}"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def add_radix(value, symbol="’"):
|
|
163
|
-
"""Add radix to a number"""
|
|
164
|
-
|
|
165
|
-
if value is None:
|
|
166
|
-
return None
|
|
167
|
-
|
|
168
|
-
value = str(value)
|
|
169
|
-
|
|
170
|
-
if "." in value:
|
|
171
|
-
integer, fractional = value.split(".")
|
|
172
|
-
else:
|
|
173
|
-
integer = value
|
|
174
|
-
fractional = ""
|
|
175
|
-
|
|
176
|
-
if integer[0] == "-":
|
|
177
|
-
sign = "-"
|
|
178
|
-
integer = integer[1:]
|
|
179
|
-
# elif integer[0] == '+':
|
|
180
|
-
# sign = '+'
|
|
181
|
-
# integer = integer[1:]
|
|
182
|
-
else:
|
|
183
|
-
sign = ""
|
|
184
|
-
|
|
185
|
-
data = ""
|
|
186
|
-
ind = 0
|
|
187
|
-
for i in integer[::-1]:
|
|
188
|
-
if ind and ind % 3 == 0:
|
|
189
|
-
data = symbol + data
|
|
190
|
-
ind += 1
|
|
191
|
-
data = i + data
|
|
192
|
-
|
|
193
|
-
data = sign + data
|
|
194
|
-
if fractional:
|
|
195
|
-
data += "." + fractional
|
|
196
|
-
|
|
197
|
-
return data
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def mul(x, y):
|
|
201
|
-
"""Multiply fractions correctly"""
|
|
202
|
-
if x is None or y is None:
|
|
203
|
-
return None
|
|
204
|
-
return float(Decimal(str(x)) * Decimal(str(y)))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def div(x, y):
|
|
208
|
-
"""Divide fractions correctly"""
|
|
209
|
-
if x is None or y is None:
|
|
210
|
-
return None
|
|
211
|
-
return float(Decimal(str(x)) / Decimal(str(y)))
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def add(x, y):
|
|
215
|
-
"""Subtract fractions correctly"""
|
|
216
|
-
if x is None or y is None:
|
|
217
|
-
return None
|
|
218
|
-
return float(Decimal(str(x)) + Decimal(str(y)))
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def sub(x, y):
|
|
222
|
-
"""Subtract fractions correctly"""
|
|
223
|
-
if x is None or y is None:
|
|
224
|
-
return None
|
|
225
|
-
return float(Decimal(str(x)) - Decimal(str(y)))
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def to_step(value, step=1, side=False):
|
|
229
|
-
"""Change value step"""
|
|
230
|
-
|
|
231
|
-
if value is None:
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
value = div(value, step)
|
|
235
|
-
if side:
|
|
236
|
-
value = math.ceil(value)
|
|
237
|
-
else:
|
|
238
|
-
value = math.floor(value)
|
|
239
|
-
value = mul(value, step)
|
|
240
|
-
|
|
241
|
-
if step >= 1:
|
|
242
|
-
value = int(value)
|
|
243
|
-
|
|
244
|
-
return value
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|