Coverage for pycommons / strings / string_conv.py: 100%
86 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
1"""Converting stuff to and from strings."""
3from datetime import datetime
4from math import isfinite, isnan
5from typing import Callable, Final, cast
7from pycommons.math.int_math import __try_int
8from pycommons.strings.chars import NBDASH, NBSP
9from pycommons.types import type_error
11#: fast call to :meth:`str.__len__`
12__LEN: Final[Callable[[str], int]] = cast("Callable[[str], int]", str.__len__)
15def float_to_str(value: float) -> str:
16 """
17 Convert `float` to a string.
19 The floating point value `value` is converted to a string.
21 :param value: the floating point value
22 :returns: the string representation
23 :raises TypeError: if `value` is not a `float`
24 :raises ValueError: if `value` is not a number
26 >>> float_to_str(1.3)
27 '1.3'
28 >>> float_to_str(1.0)
29 '1'
30 >>> float_to_str(1e-5)
31 '1e-5'
33 >>> try:
34 ... float_to_str(1)
35 ... except TypeError as te:
36 ... print(te)
37 value should be an instance of float but is int, namely 1.
39 >>> try:
40 ... float_to_str(None)
41 ... except TypeError as te:
42 ... print(te)
43 value should be an instance of float but is None.
45 >>> from math import nan
46 >>> try:
47 ... float_to_str(nan)
48 ... except ValueError as ve:
49 ... print(ve)
50 nan => 'nan' is not a permitted float.
52 >>> from math import inf
53 >>> float_to_str(inf)
54 'inf'
55 >>> float_to_str(-inf)
56 '-inf'
57 >>> float_to_str(1e300)
58 '1e300'
59 >>> float_to_str(-1e300)
60 '-1e300'
61 >>> float_to_str(-1e-300)
62 '-1e-300'
63 >>> float_to_str(1e-300)
64 '1e-300'
65 >>> float_to_str(1e1)
66 '10'
67 >>> float_to_str(1e5)
68 '100000'
69 >>> float_to_str(1e10)
70 '10000000000'
71 >>> float_to_str(1e20)
72 '1e20'
73 >>> float_to_str(1e030)
74 '1e30'
75 >>> float_to_str(0.0)
76 '0'
77 >>> float_to_str(-0.0)
78 '0'
79 """
80 if not isinstance(value, float):
81 raise type_error(value, "value", float)
82 if value == 0.0: # fast track for 0
83 return "0"
84 s = float.__repr__(value)
85 if isnan(value): # nan is not permitted
86 raise ValueError(f"{value!r} => {s!r} is not a permitted float.")
87 if not isfinite(value): # +/-inf can be returned directly
88 return s
89 return str.replace(str.replace(str.replace( # simplify/remove clutter
90 s, "e-0", "e-"), "e+0", "e"), "e+", "e").removesuffix(".0")
93def bool_to_str(value: bool) -> str:
94 """
95 Convert a Boolean value to a string.
97 This function is the inverse of :func:`str_to_bool`.
99 :param value: the Boolean value
100 :returns `"T"`: if `value == True`
101 :returns `"F"`: if `value == False`
102 :raises TypeError: if `value` is not a `bool`
104 >>> print(bool_to_str(True))
105 T
106 >>> print(bool_to_str(False))
107 F
109 >>> try:
110 ... bool_to_str("t")
111 ... except TypeError as te:
112 ... print(te)
113 value should be an instance of bool but is str, namely 't'.
115 >>> try:
116 ... bool_to_str(None)
117 ... except TypeError as te:
118 ... print(te)
119 value should be an instance of bool but is None.
120 """
121 if not isinstance(value, bool):
122 raise type_error(value, "value", bool)
123 return "T" if value else "F"
126def str_to_bool(value: str) -> bool:
127 """
128 Convert a string to a boolean value.
130 This function is the inverse of :func:`bool_to_str`.
132 :param value: the string value
133 :returns `True`: if `value == "T"`
134 :returns `False`: if `value == "F"`
135 :raises TypeError: if `value` is not a string
136 :raises ValueError: if `value` is neither `T` nor `F`
138 >>> str_to_bool("T")
139 True
140 >>> str_to_bool("F")
141 False
143 >>> try:
144 ... str_to_bool("x")
145 ... except ValueError as v:
146 ... print(v)
147 Expected 'T' or 'F', but got 'x'.
149 >>> try:
150 ... str_to_bool(1)
151 ... except TypeError as te:
152 ... print(te)
153 descriptor '__len__' requires a 'str' object but received a 'int'
155 >>> try:
156 ... str_to_bool(None)
157 ... except TypeError as te:
158 ... print(te)
159 descriptor '__len__' requires a 'str' object but received a 'NoneType'
160 """
161 if __LEN(value) == 1:
162 if value == "T":
163 return True
164 if value == "F":
165 return False
166 raise ValueError(f"Expected 'T' or 'F', but got {value!r}.")
169def num_to_str(value: int | float) -> str:
170 """
171 Transform a numerical value which is either `int` or`float` to a string.
173 If `value` is an instance of `int`, the result of its conversion via `str`
174 will be returned.
175 If `value` is an instance of `bool`, a `TypeError` will be raised.
176 Otherwise, the result of :func:`~float_to_str` is returned.
177 This means that `nan` will yield a `ValueError` and anything that is
178 neither an `int`, `bool`, or `float` will incur a `TypeError`.
180 :param value: the value
181 :returns: the string
182 :raises TypeError: if `value` is a `bool` (notice that `bool` is a
183 subclass of `int`) or any other type that is neither `int` nor
184 `float`.
185 :raises ValueError: if `value` is not-a-number
187 >>> num_to_str(1)
188 '1'
189 >>> num_to_str(1.5)
190 '1.5'
192 >>> try:
193 ... num_to_str(True)
194 ... except TypeError as te:
195 ... print(te)
196 value should be an instance of any in {float, int} but is bool, \
197namely True.
199 >>> try:
200 ... num_to_str(False)
201 ... except TypeError as te:
202 ... print(te)
203 value should be an instance of any in {float, int} but is bool, \
204namely False.
206 >>> try:
207 ... num_to_str("x")
208 ... except TypeError as te:
209 ... print(te)
210 value should be an instance of float but is str, namely 'x'.
212 >>> try:
213 ... num_to_str(None)
214 ... except TypeError as te:
215 ... print(te)
216 value should be an instance of float but is None.
218 >>> from math import inf, nan
219 >>> try:
220 ... num_to_str(nan)
221 ... except ValueError as ve:
222 ... print(ve)
223 nan => 'nan' is not a permitted float.
225 >>> num_to_str(inf)
226 'inf'
227 >>> num_to_str(-inf)
228 '-inf'
229 """
230 if isinstance(value, bool):
231 raise type_error(value, "value", (int, float))
232 return int.__str__(value) if isinstance(value, int) \
233 else float_to_str(value)
236def bool_or_num_to_str(value: int | float | bool) -> str:
237 """
238 Convert a `bool` or number to string.
240 :param value: the number or `bool`
241 :returns: the string
242 :raises TypeError: if the number is neither `bool`, `float`, or `int`.
244 >>> bool_or_num_to_str(True)
245 'T'
246 >>> bool_or_num_to_str(False)
247 'F'
248 >>> bool_or_num_to_str(12.0)
249 '12'
250 >>> bool_or_num_to_str(12)
251 '12'
252 >>> bool_or_num_to_str(12.5)
253 '12.5'
254 >>> try:
255 ... bool_or_num_to_str("x")
256 ... except TypeError as te:
257 ... print(te)
258 value should be an instance of float but is str, namely 'x'.
259 """
260 return bool_to_str(value) if isinstance(value, bool) else (
261 int.__str__(value) if isinstance(value, int) else float_to_str(value))
264def num_or_none_to_str(value: int | float | None) -> str:
265 """
266 Convert a numerical type (`int`, `float`) or `None` to a string.
268 If `value is None`, then `""` is returned.
269 Otherwise, the result of :func:`~num_to_str` is returned.
271 :param value: the value
272 :returns: the string representation, `""` for `None`
273 :returns `""`: if `value is None`
274 :returns `num_to_str(value)`: otherwise
275 :raises TypeError: if `value` not `Nont` but instead is a `bool`
276 (notice that `bool` is a subclass of `int`) or any other type that
277 is neither `int` nor `float`.
278 :raises ValueError: if `value` is not-a-number
280 >>> print(repr(num_or_none_to_str(None)))
281 ''
282 >>> print(num_or_none_to_str(12))
283 12
284 >>> print(num_or_none_to_str(12.3))
285 12.3
287 >>> try:
288 ... num_or_none_to_str(True)
289 ... except TypeError as te:
290 ... print(te)
291 value should be an instance of any in {float, int} but is bool, \
292namely True.
294 >>> try:
295 ... num_or_none_to_str(False)
296 ... except TypeError as te:
297 ... print(te)
298 value should be an instance of any in {float, int} but is bool, \
299namely False.
301 >>> from math import nan
302 >>> try:
303 ... num_to_str(nan)
304 ... except ValueError as ve:
305 ... print(ve)
306 nan => 'nan' is not a permitted float.
307 """
308 return "" if value is None else num_to_str(value)
311def int_or_none_to_str(value: int | None) -> str:
312 """
313 Convert an integer or `None` to a string.
315 If `value is None`, `""` is returned.
316 If `value` is an instance of `bool`, a `TypeError` is raised.
317 If `value` is an `int`, `str(val)` is returned.
318 Otherwise, a `TypeError` is thrown.
320 :param value: the value
321 :returns: the string representation, `''` for `None`
322 :returns `""`: if `value is None`
323 :returns `int.__str__(value)`: otherwise
324 :raises TypeError: if `value` is a `bool` (notice that `bool` is a
325 subclass of `int`) or any other non-`int` type.
327 >>> print(repr(int_or_none_to_str(None)))
328 ''
329 >>> print(int_or_none_to_str(12))
330 12
332 >>> try:
333 ... int_or_none_to_str(True)
334 ... except TypeError as te:
335 ... print(te)
336 value should be an instance of int but is bool, namely True.
338 >>> try:
339 ... int_or_none_to_str(False)
340 ... except TypeError as te:
341 ... print(te)
342 value should be an instance of int but is bool, namely False.
344 >>> print(int_or_none_to_str(-10))
345 -10
347 >>> try:
348 ... int_or_none_to_str(1.0)
349 ... except TypeError as te:
350 ... print(te)
351 value should be an instance of int but is float, namely 1.0.
352 """
353 if value is None:
354 return ""
355 if isinstance(value, bool) or (not isinstance(value, int)):
356 raise type_error(value, "value", int)
357 return int.__str__(value)
360def __str_to_num_or_none(value: str | None,
361 none_is_ok: bool) -> int | float | None:
362 """
363 Convert a string to an `int` or `float`.
365 If `value is None` and `none_is_ok == True`, then `None` is returned.
366 If `value` is not an instance of `str`, a `TypeError` will be raised.
367 If `value` becomes an empty string after stripping, then `None` is
368 returned if `none_is_ok == True` and else an `ValueError` is raised.
369 If the value `value` can be converted to an integer without loss of
370 precision, then an `int` with the corresponding value is returned.
371 If the value `value` can be converted to a `float`, a `float` with the
372 appropriate value is returned.
373 Otherwise, a `ValueError` is thrown.
375 :param value: the string value
376 :returns: the `int` or `float` or `None` corresponding to `value`
378 >>> print(type(__str_to_num_or_none("15.0", False)))
379 <class 'int'>
380 >>> print(type(__str_to_num_or_none("15.1", False)))
381 <class 'float'>
382 >>> __str_to_num_or_none("inf", False)
383 inf
384 >>> __str_to_num_or_none(" -inf ", False)
385 -inf
387 >>> try:
388 ... __str_to_num_or_none(21, False)
389 ... except TypeError as te:
390 ... print(te)
391 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
393 >>> try:
394 ... __str_to_num_or_none("nan", False)
395 ... except ValueError as ve:
396 ... print(ve)
397 NaN is not permitted, but got 'nan'.
399 >>> try:
400 ... __str_to_num_or_none("12-3", False)
401 ... except ValueError as ve:
402 ... print(ve)
403 Invalid numerical value '12-3'.
405 >>> __str_to_num_or_none("1e34423", False)
406 inf
407 >>> __str_to_num_or_none("-1e34423", False)
408 -inf
409 >>> __str_to_num_or_none("-1e-34423", False)
410 0
411 >>> __str_to_num_or_none("1e-34423", False)
412 0
414 >>> try:
415 ... __str_to_num_or_none("-1e-34e4423", False)
416 ... except ValueError as ve:
417 ... print(ve)
418 Invalid numerical value '-1e-34e4423'.
420 >>> try:
421 ... __str_to_num_or_none("T", False)
422 ... except ValueError as ve:
423 ... print(ve)
424 Invalid numerical value 'T'.
426 >>> try:
427 ... __str_to_num_or_none("F", False)
428 ... except ValueError as ve:
429 ... print(ve)
430 Invalid numerical value 'F'.
432 >>> try:
433 ... __str_to_num_or_none(None, False)
434 ... except TypeError as te:
435 ... print(te)
436 descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object
438 >>> try:
439 ... __str_to_num_or_none(" ", False)
440 ... except ValueError as ve:
441 ... print(ve)
442 Value ' ' becomes empty after stripping, cannot be converted to a number.
444 >>> try:
445 ... __str_to_num_or_none("", False)
446 ... except ValueError as ve:
447 ... print(ve)
448 Value '' becomes empty after stripping, cannot be converted to a number.
450 >>> print(__str_to_num_or_none(" ", True))
451 None
452 >>> print(__str_to_num_or_none("", True))
453 None
454 >>> print(__str_to_num_or_none(None, True))
455 None
456 """
457 if (value is None) and none_is_ok:
458 return None
459 vv: Final[str] = str.lower(str.strip(value))
460 if __LEN(vv) <= 0:
461 if none_is_ok:
462 return None
463 raise ValueError(f"Value {value!r} becomes empty after stripping, "
464 "cannot be converted to a number.")
465 try:
466 return int(vv)
467 except ValueError:
468 pass
469 res: float
470 try:
471 res = float(vv)
472 except ValueError as ve:
473 raise ValueError(f"Invalid numerical value {value!r}.") from ve
474 if isnan(res):
475 raise ValueError(f"NaN is not permitted, but got {value!r}.")
476 return __try_int(res)
479def str_to_num(value: str) -> int | float:
480 """
481 Convert a string to an `int` or `float`.
483 If `value` is not an instance of `str`, a `TypeError` will be raised.
484 If the value `value` can be converted to an integer, then an `int` with
485 the corresponding value is returned.
486 If the value `value` can be converted to a `float`, a `float` with the
487 appropriate value is returned.
488 Otherwise, a `ValueError` is thrown.
490 :param value: the string value
491 :returns: the `int` or `float`: Integers are preferred to be used whereever
492 possible
493 :raises TypeError: if `value` is not a `str`
494 :raises ValueError: if `value` is a `str` but cannot be converted to an
495 integer (base-10) or converts to a `float` which is not a number
497 >>> print(type(str_to_num("15.0")))
498 <class 'int'>
499 >>> print(type(str_to_num("15.1")))
500 <class 'float'>
501 >>> str_to_num("inf")
502 inf
503 >>> str_to_num(" -inf ")
504 -inf
505 >>> try:
506 ... str_to_num(21)
507 ... except TypeError as te:
508 ... print(te)
509 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
510 >>> try:
511 ... str_to_num("nan")
512 ... except ValueError as ve:
513 ... print(ve)
514 NaN is not permitted, but got 'nan'.
516 >>> try:
517 ... str_to_num("12-3")
518 ... except ValueError as ve:
519 ... print(ve)
520 Invalid numerical value '12-3'.
522 >>> str_to_num("1e34423")
523 inf
524 >>> str_to_num("-1e34423")
525 -inf
526 >>> str_to_num("-1e-34423")
527 0
528 >>> str_to_num("1e-34423")
529 0
530 >>> try:
531 ... str_to_num("-1e-34e4423")
532 ... except ValueError as ve:
533 ... print(ve)
534 Invalid numerical value '-1e-34e4423'.
536 >>> try:
537 ... str_to_num("T")
538 ... except ValueError as ve:
539 ... print(ve)
540 Invalid numerical value 'T'.
542 >>> try:
543 ... str_to_num("F")
544 ... except ValueError as ve:
545 ... print(ve)
546 Invalid numerical value 'F'.
548 >>> try:
549 ... str_to_num(None)
550 ... except TypeError as te:
551 ... print(te)
552 descriptor 'strip' for 'str' objects doesn't apply to a 'NoneType' object
553 >>> try:
554 ... str_to_num("")
555 ... except ValueError as ve:
556 ... print(ve)
557 Value '' becomes empty after stripping, cannot be converted to a number.
558 """
559 return __str_to_num_or_none(value, False)
562def str_to_num_or_none(value: str | None) -> int | float | None:
563 """
564 Convert a string to an `int` or `float` or `None`.
566 If the value `value` is `None`, then `None` is returned.
567 If the vlaue `value` is empty or entirely composed of white space, `None`
568 is returned.
569 If the value `value` can be converted to an integer, then an `int` with
570 the corresponding value is returned.
571 If the value `value` can be converted to a `float`, a `float` with the
572 appropriate value is returned.
573 Otherwise, a `ValueError` is thrown.
575 :param value: the string value
576 :returns: the `int` or `float` or `None`
577 :raises TypeError: if `value` is neither a `str` nor `None`
578 :raises ValueError: if `value` is a `str` but cannot be converted to an
579 integer (base-10) or converts to a `float` which is not a number
581 >>> print(type(str_to_num_or_none("15.0")))
582 <class 'int'>
583 >>> print(type(str_to_num_or_none("15.1")))
584 <class 'float'>
585 >>> str_to_num_or_none("inf")
586 inf
587 >>> str_to_num_or_none(" -inf ")
588 -inf
589 >>> try:
590 ... str_to_num_or_none(21)
591 ... except TypeError as te:
592 ... print(te)
593 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object
594 >>> try:
595 ... str_to_num_or_none("nan")
596 ... except ValueError as ve:
597 ... print(ve)
598 NaN is not permitted, but got 'nan'.
600 >>> try:
601 ... str_to_num_or_none("12-3")
602 ... except ValueError as ve:
603 ... print(ve)
604 Invalid numerical value '12-3'.
606 >>> str_to_num_or_none("1e34423")
607 inf
608 >>> str_to_num_or_none("-1e34423")
609 -inf
610 >>> str_to_num_or_none("-1e-34423")
611 0
612 >>> str_to_num_or_none("1e-34423")
613 0
614 >>> try:
615 ... str_to_num_or_none("-1e-34e4423")
616 ... except ValueError as ve:
617 ... print(ve)
618 Invalid numerical value '-1e-34e4423'.
620 >>> try:
621 ... str_to_num_or_none("T")
622 ... except ValueError as ve:
623 ... print(ve)
624 Invalid numerical value 'T'.
626 >>> try:
627 ... str_to_num_or_none("F")
628 ... except ValueError as ve:
629 ... print(ve)
630 Invalid numerical value 'F'.
632 >>> print(str_to_num_or_none(""))
633 None
634 >>> print(str_to_num_or_none(None))
635 None
636 >>> print(type(str_to_num_or_none("5.0")))
637 <class 'int'>
638 >>> print(type(str_to_num_or_none("5.1")))
639 <class 'float'>
640 """
641 return __str_to_num_or_none(value, True)
644def str_to_int_or_none(value: str | None) -> int | None:
645 """
646 Convert a string to an `int` or `None`.
648 If the value `value` is `None`, then `None` is returned.
649 If the vlaue `value` is empty or entirely composed of white space, `None`
650 is returned.
651 If the value `value` can be converted to an integer, then an `int` with
652 the corresponding value is returned.
653 Otherwise, a `ValueError` is thrown.
655 :param value: the string value, or `None`
656 :returns: the int or None
657 :raises TypeError: if `value` is neither a `str` nor `None`
658 :raises ValueError: if `value` is a `str` but cannot be base-10 converted
659 to an integer
661 >>> print(str_to_int_or_none(""))
662 None
663 >>> print(str_to_int_or_none("5"))
664 5
665 >>> print(str_to_int_or_none(None))
666 None
667 >>> print(str_to_int_or_none(" "))
668 None
669 >>> try:
670 ... print(str_to_int_or_none(1.3))
671 ... except TypeError as te:
672 ... print(te)
673 value should be an instance of str but is float, namely 1.3.
675 >>> try:
676 ... print(str_to_int_or_none("1.3"))
677 ... except ValueError as ve:
678 ... print(ve)
679 invalid literal for int() with base 10: '1.3'
680 """
681 if value is None:
682 return None
683 if not isinstance(value, str):
684 raise type_error(value, "value", str)
685 vv: Final[str] = value.strip().lower()
686 if len(vv) <= 0:
687 return None
688 return int(vv)
691#: the format for time
692__DATE_FORMAT: Final[str] = f"%Y{NBDASH}%m{NBDASH}%d"
693#: the format for date and time
694__DATE_TIME_FORMAT_NO_TZ: Final[str] = f"{__DATE_FORMAT}{NBSP}%H:%M"
695#: the format for date and time
696__DATE_TIME_FORMAT_TZ: Final[str] = f"{__DATE_TIME_FORMAT_NO_TZ}{NBSP}%Z"
699def datetime_to_date_str(date: datetime) -> str:
700 """
701 Convert a datetime object to a date string.
703 :param date: the date
704 :returns: the date string
705 :raises TypeError: if `date` is not an instance of
706 :class:`datetime.datetime`.
708 >>> datetime_to_date_str(datetime(1999, 12, 21))
709 '1999\u201112\u201121'
710 >>> try:
711 ... datetime_to_date_str(None)
712 ... except TypeError as te:
713 ... print(te)
714 date should be an instance of datetime.datetime but is None.
716 >>> try:
717 ... datetime_to_date_str(1)
718 ... except TypeError as te:
719 ... print(te)
720 date should be an instance of datetime.datetime but is int, namely 1.
721 """
722 if not isinstance(date, datetime):
723 raise type_error(date, "date", datetime)
724 return date.strftime(__DATE_FORMAT)
727def datetime_to_datetime_str(dateandtime: datetime) -> str:
728 r"""
729 Convert a datetime object to a date-time string.
731 :param dateandtime: the date and time
732 :returns: the date-time string
733 :raises TypeError: if `dateandtime` is not an instance of
734 :class:`datetime.datetime`.
736 >>> datetime_to_datetime_str(datetime(1999, 12, 21, 13, 42, 23))
737 '1999\u201112\u201121\xa013:42'
738 >>> from datetime import timezone
739 >>> datetime_to_datetime_str(datetime(1999, 12, 21, 13, 42,
740 ... tzinfo=timezone.utc))
741 '1999\u201112\u201121\xa013:42\xa0UTC'
742 >>> try:
743 ... datetime_to_datetime_str(None)
744 ... except TypeError as te:
745 ... print(te)
746 dateandtime should be an instance of datetime.datetime but is None.
748 >>> try:
749 ... datetime_to_datetime_str(1)
750 ... except TypeError as te:
751 ... print(str(te)[:60])
752 dateandtime should be an instance of datetime.datetime but i
753 """
754 if not isinstance(dateandtime, datetime):
755 raise type_error(dateandtime, "dateandtime", datetime)
756 return dateandtime.strftime(
757 __DATE_TIME_FORMAT_NO_TZ if dateandtime.tzinfo is None
758 else __DATE_TIME_FORMAT_TZ)