batutils 2.0.0__py3-none-any.whl

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.
Files changed (73) hide show
  1. batutils/__init__.py +5 -0
  2. batutils/_common/__init__.py +6 -0
  3. batutils/_common/_decode.py +40 -0
  4. batutils/dtu/__init__.py +19 -0
  5. batutils/dtu/casting/__init__.py +5 -0
  6. batutils/dtu/casting/_any.py +136 -0
  7. batutils/dtu/casting/_filetime.py +36 -0
  8. batutils/dtu/casting/_instant.py +31 -0
  9. batutils/dtu/casting/_string.py +81 -0
  10. batutils/dtu/casting/_timestamp.py +50 -0
  11. batutils/dtu/formatting/__init__.py +2 -0
  12. batutils/dtu/formatting/_delta.py +62 -0
  13. batutils/dtu/formatting/_serialize.py +26 -0
  14. batutils/dtu/parsers/__init__.py +2 -0
  15. batutils/dtu/parsers/_string.py +44 -0
  16. batutils/dtu/parsers/_timedelta.py +161 -0
  17. batutils/dtu/shortcuts/__init__.py +3 -0
  18. batutils/dtu/shortcuts/_aliases.py +29 -0
  19. batutils/dtu/shortcuts/_checkers.py +21 -0
  20. batutils/dtu/shortcuts/_finders.py +80 -0
  21. batutils/dtu/structs/__init__.py +5 -0
  22. batutils/dtu/structs/_base.py +5 -0
  23. batutils/dtu/structs/_deltasplit.py +278 -0
  24. batutils/dtu/structs/_types.py +24 -0
  25. batutils/dtu/structs/aliases.py +21 -0
  26. batutils/dtu/structs/consts.py +35 -0
  27. batutils/structs/__init__.py +7 -0
  28. batutils/structs/_base.py +21 -0
  29. batutils/tpu/__init__.py +0 -0
  30. batutils/tpu/casting/__init__.py +9 -0
  31. batutils/tpu/casting/_currency.py +52 -0
  32. batutils/tpu/casting/_evals.py +247 -0
  33. batutils/tpu/casting/_ip.py +58 -0
  34. batutils/tpu/casting/_mask.py +34 -0
  35. batutils/tpu/casting/_params.py +108 -0
  36. batutils/tpu/casting/_serialize.py +20 -0
  37. batutils/tpu/comparison/__init__.py +1 -0
  38. batutils/tpu/comparison/_mixin.py +30 -0
  39. batutils/tpu/insp/__init__.py +2 -0
  40. batutils/tpu/insp/_insp_old.py +347 -0
  41. batutils/tpu/insp/_typecheck.py +89 -0
  42. batutils/tpu/iters/__init__.py +4 -0
  43. batutils/tpu/iters/_attrs.py +84 -0
  44. batutils/tpu/iters/_dicts.py +130 -0
  45. batutils/tpu/iters/_group.py +50 -0
  46. batutils/tpu/iters/_lists.py +178 -0
  47. batutils/tpu/size/__init__.py +1 -0
  48. batutils/tpu/size/_total.py +52 -0
  49. batutils/tpu/strimp/__init__.py +1 -0
  50. batutils/tpu/strimp/_getters.py +285 -0
  51. batutils/tpu/string/__init__.py +1 -0
  52. batutils/tpu/string/_string.py +160 -0
  53. batutils/tpu/structs/__init__.py +1 -0
  54. batutils/tpu/structs/_base.py +3 -0
  55. batutils/tpu/structs/empty/__init__.py +1 -0
  56. batutils/tpu/structs/empty/_empty.py +387 -0
  57. batutils/tpu/structs/enumex/__init__.py +1 -0
  58. batutils/tpu/structs/enumex/_enumextra.py +25 -0
  59. batutils/tpu/structs/fluid/__init__.py +1 -0
  60. batutils/tpu/structs/fluid/_attrfluid.py +77 -0
  61. batutils/tpu/structs/sentience/__init__.py +1 -0
  62. batutils/tpu/structs/sentience/_builder.py +87 -0
  63. batutils/tpu/structs/serializers/__init__.py +3 -0
  64. batutils/tpu/structs/serializers/_json.py +12 -0
  65. batutils/tpu/structs/serializers/_jsonsafe.py +69 -0
  66. batutils/tpu/structs/serializers/_universal.py +318 -0
  67. batutils/tpu/structs/singleton/__init__.py +1 -0
  68. batutils/tpu/structs/singleton/_singleton.py +35 -0
  69. batutils-2.0.0.dist-info/LICENSE +22 -0
  70. batutils-2.0.0.dist-info/METADATA +125 -0
  71. batutils-2.0.0.dist-info/RECORD +73 -0
  72. batutils-2.0.0.dist-info/WHEEL +5 -0
  73. batutils-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,161 @@
1
+ __all__ = [
2
+ 'str_to_delta',
3
+ ]
4
+ from batutils.dtu.structs import *
5
+
6
+ _PERIOD_PART = re.compile(r"([+-]?(?:\d+(?:\.(?:\d+)?)?|\.\d+))\s*([a-z]+)\s*", re.IGNORECASE)
7
+
8
+ _KEYWORDS = {
9
+ 'd': 'days',
10
+ 'day': 'days',
11
+ 'days': 'days',
12
+
13
+ 'h': 'hours',
14
+ 'hr': 'hours',
15
+ 'hrs': 'hours',
16
+ 'hour': 'hours',
17
+ 'hours': 'hours',
18
+
19
+ 'm': 'minutes',
20
+ 'i': 'minutes',
21
+ 'min': 'minutes',
22
+ 'mins': 'minutes',
23
+ 'minute': 'minutes',
24
+ 'minutes': 'minutes',
25
+
26
+ 's': 'seconds',
27
+ 'sec': 'seconds',
28
+ 'secs': 'seconds',
29
+ 'second': 'seconds',
30
+ 'seconds': 'seconds',
31
+
32
+ 'w': 'weeks',
33
+ 'wk': 'weeks',
34
+ 'wks': 'weeks',
35
+ 'week': 'weeks',
36
+ 'weeks': 'weeks',
37
+ }
38
+
39
+
40
+ def _parse_split(string: str) -> Optional[List[Tuple[str, str]]]:
41
+ """Splits a string presumed to containing a written out time period into a
42
+ list of 2-tuples, each containing a numerical value first in string format
43
+ and then a unit keyword.
44
+
45
+ Strictly speaking this just searches for any number in a text (integer or
46
+ decimal, with an optional leading + or -) and the first word following that
47
+ number and pairs them up, but that basically does the job shockingly well.
48
+
49
+ Examples:
50
+ >>> _parse_split('2 days 7 hours 10 minutes')
51
+ [('2', 'days'), ('7', 'hours'), ('10', 'minutes')]
52
+
53
+ >>> _parse_split('2 days, 7.3 hours and 10 minutes')
54
+ [('2', 'days'), ('7.3', 'hours'), ('10', 'minutes')]
55
+
56
+ >>> _parse_split('2 days, 7.3 hrs and 10 mins')
57
+ [('2', 'days'), ('7.3', 'hrs'), ('10', 'mins')]
58
+
59
+ >>> _parse_split('2 days, -7.3 hrs and 10 floops')
60
+ [('2', 'days'), ('-7.3', 'hrs'), ('10', 'floops')]
61
+
62
+ >>> _parse_split('2d-7.3h10m')
63
+ [('2', 'd'), ('-7.3', 'h'), ('10', 'm')]
64
+
65
+ >>> _parse_split('1st of all, the 3 bears were really just 2 and-a-half bears')
66
+ [('1', 'st'), ('3', 'bears'), ('2', 'and')]
67
+ """
68
+ if string and isinstance(string, str):
69
+ buf = []
70
+ string = string.strip().lower()
71
+ for match in _PERIOD_PART.finditer(string):
72
+ buf.append(match.groups())
73
+ return buf or None
74
+ return None
75
+
76
+
77
+ def _timedelta_keywords(list_of_lists: List[Tuple[str, str]]) -> Dict:
78
+ """Given a list of 2-tuples, containing the numerical value first in string
79
+ format and then a time unit keyword, parses the numerical value to a Python
80
+ float and changes any time unit format alias or shorthand into its
81
+ equivalent keyword that's a valid argument for initialising a timedelta,
82
+ and then pairs up each of those into a dict that can then be fed into
83
+ timedelta's __init__ as kwargs via the ** operator.
84
+
85
+ :raises KeyError: If a time unit keyword was not recognised
86
+ """
87
+ kw_map = {}
88
+ for num, kw in list_of_lists:
89
+ kw_map[_KEYWORDS[kw]] = float(num)
90
+ return kw_map
91
+
92
+
93
+ def split_delta(delta, include_weeks=True, include_months=True, include_years=True):
94
+ """Splits a timedelta into the time periods it approximately
95
+ contains. Mean length of years and months are used and fractional
96
+ days/seconds might get shaved of but this is useful to evaluate individual
97
+ period lengths of displaying the maximum chunk of period a timedelta
98
+ contains.
99
+
100
+ This class includes an `is_past` property which is True if the timedelta
101
+ contained a negative value but all the values in the split are nevertheless
102
+ kept positive.
103
+
104
+ It also includes a few `largest` and `second_largest` properties that
105
+ contain the first and second period units that contain values greater than 0
106
+ in order of increasing accuracy (i.e. from year to second).
107
+
108
+ Example:
109
+
110
+ >>> total = datetime.timedelta(days=2002, seconds=20000)
111
+ >>> parts = split_delta(total)
112
+ >>> parts
113
+ {'seconds': 20, 'months': 5, 'days': 2, '2nd': 'months', 'hours': 5, '1st': 'years', 'is_past': False, 'weeks': 3, 'years': 5, 'minutes': 33}
114
+
115
+ :param delta: Time period to evaluate
116
+ :type delta: datetime.timedelta
117
+ :param include_weeks: Include weeks in the division (and reduce the days accordingly)?
118
+ :type include_weeks: bool
119
+ :rtype: dict
120
+ """
121
+ ds = DeltaSplit(time_delta=delta, include_weeks=include_weeks, include_months=include_months, include_years=include_years)
122
+ parts = ds.to_dict()
123
+ parts['1st'] = parts.pop('largest')
124
+ parts['2nd'] = parts.pop('second_largest')
125
+ return parts
126
+
127
+
128
+
129
+ def str_to_delta(string: str, default: Any = 0) -> Union[TimeDelta, Any]:
130
+ """Converts a simple string with time duration keywords into a
131
+ `datetime.timedelta` object, akin to the keyword arguments in
132
+ `datetime.timedelta`.
133
+
134
+ Examples:
135
+ >>> str_to_delta('12days') == datetime.timedelta(days=12)
136
+ True
137
+ >>> str_to_delta('2h34m') == datetime.timedelta(hours=2, minutes=34)
138
+ True
139
+ >>> str_to_delta('-1.5 hours') == datetime.timedelta(hours=-1.5)
140
+ True
141
+
142
+ Valid keywords (and aliases):
143
+ - days (d, day)
144
+ - hours (h, hr, hrs, hour)
145
+ - minutes (m, i, min, mins, minute)
146
+ - seconds (s, sec, secs, second)
147
+ - weeks (w, wk, wks, week)
148
+
149
+ Numeric values can be integers or floats (with or without a plus or minus
150
+ sign) and all whitespaces are valid but ignored.
151
+ """
152
+ if default == 0:
153
+ default = TimeDelta(seconds=0)
154
+ res = _parse_split(string)
155
+ if res:
156
+ try:
157
+ return TimeDelta(**_timedelta_keywords(res))
158
+ except (ValueError, KeyError, IndexError):
159
+ return default
160
+ else:
161
+ return default
@@ -0,0 +1,3 @@
1
+ from ._aliases import *
2
+ from ._checkers import *
3
+ from ._finders import *
@@ -0,0 +1,29 @@
1
+ __all__ = [
2
+ 'now',
3
+ 'now_time',
4
+ 'today',
5
+ 'ticks',
6
+ ]
7
+
8
+ from batutils.dtu.structs import *
9
+ import time
10
+
11
+
12
+ def now() -> Datetime:
13
+ """Shortcut alias to `datetime.datetime.now()`"""
14
+ return Datetime.now()
15
+
16
+
17
+ def now_time() -> Time:
18
+ """Shortcut alias to `datetime.datetime.now().time()`"""
19
+ return Datetime.now().time()
20
+
21
+
22
+ def today() -> Date:
23
+ """Shortcut alias to `datetime.date.today()`"""
24
+ return Date.today()
25
+
26
+
27
+ def ticks() -> float:
28
+ """Shortcut alias to `time.time()`"""
29
+ return time.time()
@@ -0,0 +1,21 @@
1
+ __all__ = [
2
+ 'is_past',
3
+ 'is_future',
4
+ ]
5
+
6
+ from batutils.dtu.structs import *
7
+ from batutils.dtu.casting import *
8
+ from ._aliases import *
9
+
10
+
11
+ def is_past(temporal_object: T_TEMPORAL_VALUE) -> bool:
12
+ """Returns true if the given temporal value is in the past.
13
+ """
14
+ return any_to_datetime(temporal_object) < now()
15
+
16
+
17
+ def is_future(temporal_object: T_TEMPORAL_VALUE) -> bool:
18
+ """Returns true if the given temporal value is in the future.
19
+ """
20
+ return any_to_datetime(temporal_object) > now()
21
+
@@ -0,0 +1,80 @@
1
+ __all__ = [
2
+ 'next_time',
3
+ 'next_weekday',
4
+ 'set_midnight',
5
+ 'from_now',
6
+ 'ago',
7
+ ]
8
+
9
+ from batutils.dtu.structs import *
10
+
11
+
12
+ def next_time(find_time: Time, start_dt: Optional[Datetime] = None) -> Datetime:
13
+ """Find the earlier datetime when the given `find_time` occurs, after an
14
+ optional given `start_dt`, which is the current datetime by default.
15
+
16
+ Useful if you need something like "When does the next happy hour start?" if
17
+ "Happy-Hour" starts at 17:00 every day.
18
+
19
+ So if it's 2023-09-02 and 13:23:57...
20
+ >>> next_time(Time(17, 0))
21
+ datetime.datetime(2023, 9, 2, 17, 0, 0, 0)
22
+
23
+ But if its past 17, or 2023-09-02 and 19:23:57, we get tomorrow...
24
+ >>> next_time(Time(17, 0))
25
+ datetime.datetime(2023, 9, 3, 17, 0, 0, 0)
26
+ """
27
+ start_dt = start_dt or Datetime.now()
28
+ dt_with_time_replaced = Datetime.combine(start_dt.date(), find_time)
29
+ if dt_with_time_replaced <= start_dt:
30
+ # New time is BEFORE the original time, so move it forward by one day
31
+ dt_with_time_replaced += TimeDelta(days=1)
32
+ return dt_with_time_replaced
33
+
34
+
35
+ def next_weekday(day_of_week: Union[Weekday, int],
36
+ find_time: Optional[Time] = None,
37
+ start_dt: Optional[Datetime] = None) -> Datetime:
38
+ """Find the earlier datetime occurrence of the given `day_of_week`
39
+ (where 0 = Monday) after an optional given `start_dt`, which is the current
40
+ datetime by default and using the optional given `find_time`, which is
41
+ midnight by default.
42
+ """
43
+ start_dt = start_dt or Datetime.now() # Today
44
+ find_time = find_time or Time() # Midnight
45
+ dt_with_time_replaced = datetime.datetime.combine(start_dt.date(), find_time)
46
+ if dt_with_time_replaced <= start_dt:
47
+ # New time is BEFORE the original time, so move it forward by one day
48
+ dt_with_time_replaced += datetime.timedelta(days=1)
49
+
50
+ # Now we need to advance enough days to land on the correct week day
51
+ days_to_advance = (day_of_week - dt_with_time_replaced.weekday()) % 7
52
+ return dt_with_time_replaced + datetime.timedelta(days=days_to_advance)
53
+
54
+
55
+ def set_midnight(dt: T_DATE_VALUE) -> Datetime:
56
+ """Rounds off any hours, minutes, seconds, etc. of a datetime object and
57
+ sets the time to midnight that day or combines a date object with a time
58
+ object set to midnight.
59
+ """
60
+ if isinstance(dt, Datetime):
61
+ dt = dt.date()
62
+ if isinstance(dt, Date):
63
+ return datetime.datetime.combine(dt, Time())
64
+ return dt
65
+
66
+
67
+ def from_now(days: T_NUMBER = 0, hours: T_NUMBER = 0, minutes: T_NUMBER = 0,
68
+ seconds: T_NUMBER = 0, weeks: T_NUMBER = 0) -> Datetime:
69
+ """Adds the given number of weeks, days, hours, minutes and/or seconds
70
+ to the current datetime and returns it.
71
+ """
72
+ return Datetime.now() + TimeDelta(days=days, hours=hours, minutes=minutes, seconds=seconds, weeks=weeks)
73
+
74
+
75
+ def ago(days: T_NUMBER = 0, hours: T_NUMBER = 0, minutes: T_NUMBER = 0,
76
+ seconds: T_NUMBER = 0, weeks: T_NUMBER = 0) -> Datetime:
77
+ """Subtracts the given number of weeks, days, hours, minutes and/or seconds
78
+ from the current datetime and returns it.
79
+ """
80
+ return Datetime.now() - TimeDelta(days=days, hours=hours, minutes=minutes, seconds=seconds, weeks=weeks)
@@ -0,0 +1,5 @@
1
+ from batutils.dtu.structs._base import *
2
+ from batutils.dtu.structs._deltasplit import *
3
+ from batutils.dtu.structs.aliases import *
4
+ from batutils.dtu.structs.consts import *
5
+ from batutils.dtu.structs._types import *
@@ -0,0 +1,5 @@
1
+ import datetime
2
+
3
+ from batutils.structs._base import * # noqa
4
+
5
+
@@ -0,0 +1,278 @@
1
+ __all__ = [
2
+ 'DeltaSplit',
3
+ ]
4
+
5
+ from ._base import *
6
+ from .aliases import *
7
+ from .consts import *
8
+ from ._types import *
9
+
10
+
11
+ _field_size_map: Dict[str, T_NUMBER] = {
12
+ 'years': DAYS_IN_MEAN_YEAR, # Days
13
+ 'months': DAYS_IN_MEAN_MONTH, # Days
14
+ 'weeks': 7, # Days
15
+
16
+ 'hours': SECONDS_IN_ONE_HOUR,
17
+ 'minutes': SECONDS_IN_ONE_MINUTE,
18
+ }
19
+
20
+ _DEFAULT_DIRECTIONALS = ('in {}', '{} ago')
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True)
24
+ class DeltaSplit:
25
+ """Splits a timedelta into the time periods it approximately
26
+ contains. Mean length of years and months are used and fractional
27
+ days/seconds might get shaved of but this is useful to evaluate individual
28
+ period lengths of displaying the maximum chunk of period a timedelta
29
+ contains.
30
+
31
+ This class includes an `is_past` property which is True if the timedelta
32
+ contained a negative value but all the values in the split are nevertheless
33
+ kept positive.
34
+
35
+ It also includes a few `largest` and `second_largest` properties that
36
+ contain the first and second period units that contain values greater than 0
37
+ in order of increasing accuracy (i.e. from year to second).
38
+ """
39
+
40
+ # Names of unit fields with non-zero values in order of unit size
41
+ field_order: Tuple[str] = dataclasses.field(default_factory=tuple, init=False)
42
+
43
+ is_past: bool = dataclasses.field(default=False, init=False) # Is this counting backwards in time?
44
+
45
+ years: Optional[int] = dataclasses.field(default=None, init=False) # Optional
46
+ months: Optional[int] = dataclasses.field(default=None, init=False) # Optional
47
+ weeks: Optional[int] = dataclasses.field(default=None, init=False) # Optional
48
+ days: int = dataclasses.field(default=0, init=False)
49
+ hours: int = dataclasses.field(default=0, init=False)
50
+ minutes: int = dataclasses.field(default=0, init=False)
51
+ seconds: int = dataclasses.field(default=0, init=False)
52
+
53
+ time_delta: dataclasses.InitVar[TimeDelta]
54
+ include_weeks: dataclasses.InitVar[bool] = True
55
+ include_months: dataclasses.InitVar[bool] = True
56
+ include_years: dataclasses.InitVar[bool] = True
57
+
58
+ def get(self, field_name: str) -> str:
59
+ return f'{self.get_value(field_name)} {self._render_field_name(field_name)}'
60
+
61
+ def get_value(self, field_name: str) -> int:
62
+ return getattr(self, field_name, 0) or 0
63
+
64
+ def _render_field_name(self, field_name: str) -> str:
65
+ if self.get_value(field_name) == 1:
66
+ return field_name[:-1] # Cut off the trailing 's'
67
+ return field_name
68
+
69
+ def _be_greedy(self, field_name: str, total: T_NUMBER) -> T_NUMBER:
70
+ object.__setattr__(self, field_name, (int(total // _field_size_map[field_name])))
71
+ return total % _field_size_map[field_name]
72
+
73
+ def __post_init__(self, time_delta: TimeDelta, include_weeks: bool = True,
74
+ include_months: bool = True, include_years: bool = True):
75
+ field_order = []
76
+ if time_delta.total_seconds() < 0:
77
+ object.__setattr__(self, 'is_past', True)
78
+ # Invert deltas in the past because they track negative days but positive seconds, which gets confusing
79
+ time_delta = datetime.timedelta(seconds=-time_delta.total_seconds())
80
+
81
+ days = abs(time_delta.days)
82
+ if include_years:
83
+ days = self._be_greedy('years', days)
84
+ if self.years:
85
+ field_order.append('years')
86
+
87
+ if include_months:
88
+ days = self._be_greedy('months', days)
89
+ if self.months:
90
+ field_order.append('months')
91
+
92
+ if include_weeks:
93
+ days = self._be_greedy('weeks', days)
94
+ if self.weeks:
95
+ field_order.append('weeks')
96
+
97
+ object.__setattr__(self, 'days', int(days))
98
+ if self.days:
99
+ field_order.append('days')
100
+
101
+ seconds = self._be_greedy('hours', abs(time_delta.seconds))
102
+ if self.hours:
103
+ field_order.append('hours')
104
+
105
+ seconds = self._be_greedy('minutes', seconds)
106
+ if self.minutes:
107
+ field_order.append('minutes')
108
+
109
+ object.__setattr__(self, 'seconds', int(seconds))
110
+ if self.seconds:
111
+ field_order.append('seconds')
112
+
113
+ object.__setattr__(self, 'field_order', tuple(field_order))
114
+
115
+ @property
116
+ def largest(self) -> str:
117
+ """
118
+ :return: The largest
119
+ """
120
+ if self.field_order:
121
+ return f'{self.largest_value} {self._render_field_name(self.largest_field_name)}'
122
+ return ''
123
+
124
+ @property
125
+ def largest_field_name(self) -> Optional[str]:
126
+ """
127
+ :return: The name of the largest unit field with a non-zero value or None if there is none.
128
+ """
129
+ if self.field_order:
130
+ return self.field_order[0]
131
+ else:
132
+ return None
133
+
134
+ @property
135
+ def largest_value(self) -> int:
136
+ """
137
+ :return: The value of the largest unit field with a non-zero value or 0.
138
+ """
139
+ return self.get_value(self.largest_field_name) if self.largest_field_name else 0
140
+
141
+ @property
142
+ def second_largest(self) -> str:
143
+ """
144
+ :return: The largest
145
+ """
146
+ if len(self.field_order) > 1:
147
+ return f'{self.second_largest_value} {self._render_field_name(self.second_largest_field_name)}'
148
+ return ''
149
+
150
+ @property
151
+ def second_largest_field_name(self) -> Optional[str]:
152
+ """
153
+ :return: The name of the second-largest unit field with a non-zero value or None if there is none.
154
+ """
155
+ if len(self.field_order) > 1:
156
+ return self.field_order[1]
157
+ else:
158
+ return None
159
+
160
+ @property
161
+ def second_largest_value(self) -> int:
162
+ """
163
+ :return: The value of the second-largest unit field with a non-zero value or 0.
164
+ """
165
+ return self.get_value(self.second_largest_field_name) if self.second_largest_field_name else 0
166
+
167
+ def to_str(self, max_number_of_fields: int = 2, include_seconds: bool = False,
168
+ directionals: Optional[Tuple[str, str]] = _DEFAULT_DIRECTIONALS,
169
+ no_time_str: str = 'no time at all',
170
+ only_sec_str: str = 'a few seconds',
171
+ granularity_halting_threshold: Optional[int] = 1) -> str:
172
+ """Returns a "human-readable" textual representation of this DeltaSplit, e.g.
173
+ - "3 weeks"
174
+ - "a few seconds"
175
+ - "1 year and 7 months"
176
+ - "7 years, 3 months, 1 week, 6 days, 34 minutes and 7 seconds"
177
+ - "no time at all"
178
+
179
+ :param max_number_of_fields: Maximum number of fields to include in the
180
+ output. There can at most be 7 fields with
181
+ values so any number than 6 will include
182
+ all possible fields. The default is 2.
183
+ :param include_seconds: By default, the seconds field is allways omitted
184
+ if there are any other larger unit fields with
185
+ non-zero values (even if `max_number_of_fields`
186
+ says it should be included) and if the only
187
+ non-zero value field is seconds, then the text
188
+ "a few seconds" is returned. If
189
+ `include seconds` is True, then the seconds
190
+ field is treated as all other fields and
191
+ returned if `max_number_of_fields` allows and
192
+ the exact number of seconds are always used.
193
+ :param directionals: A two-tuple of strings to use as final formatters
194
+ (using `{}` as placeholder for the actual string)
195
+ for future and past values respectively. By
196
+ default, this is `('in {}', '{} ago')` so future
197
+ values format like "in 3 weeks" and past like "7
198
+ days and 3 hours ago". This can be fully excluded
199
+ by setting the `directionals` to None.
200
+ :param no_time_str: The string to return if this DeltaSplit contains no
201
+ time (or less than a whole second).
202
+ :param only_sec_str: The string to return if `include_seconds` is False
203
+ and the DeltaSplit only contains seconds. Default
204
+ is `a few seconds`.
205
+ :param granularity_halting_threshold: An optional threshold value that,
206
+ if a field value exceeds, causes
207
+ the rendering to halt its granular
208
+ descent. E.g. if `granularity_halting_threshold=1`
209
+ and `max_number_of_fields=2` and
210
+ we have the fields `days=1,
211
+ hours=2, minutes=3` we'll get "1
212
+ day and 2 hours" while if `days=2,
213
+ hours=2, minutes=3` we'll just get
214
+ "2 days" because `days` exceeds
215
+ the threshold of 1 and thus halts
216
+ diving into more granular fields.
217
+ The default is `1` but setting
218
+ this to `None` will just turn this
219
+ check off entirely.
220
+ """
221
+ if not self.field_order:
222
+ return no_time_str
223
+
224
+ def _internal() -> str:
225
+ # First of all, let's take care of this case where we only have seconds!
226
+ if self.largest_field_name == 'seconds':
227
+ if include_seconds:
228
+ return self.largest # Include them
229
+ else:
230
+ return only_sec_str # Skip them
231
+
232
+ # Now for a more "generic" approach
233
+ fields = list(self.field_order[0:max_number_of_fields])
234
+ if not include_seconds and fields[-1] == 'seconds': # We have, but don't want seconds!
235
+ fields = fields[0:-1] # Remove Seconds
236
+
237
+ as_strings = []
238
+ for f in fields:
239
+ as_strings.append(self.get(f))
240
+ if granularity_halting_threshold and self.get_value(f) > granularity_halting_threshold:
241
+ break
242
+
243
+ if len(as_strings) == 1:
244
+ return as_strings[0]
245
+
246
+ elif len(as_strings) == 2:
247
+ return f'{as_strings[0]} and {as_strings[1]}'
248
+
249
+ else:
250
+ return f'{", ".join(as_strings[0:-1])} and {as_strings[-1]}'
251
+
252
+ if directionals:
253
+ if self.is_past:
254
+ return directionals[1].format(_internal())
255
+ return directionals[0].format(_internal())
256
+ return _internal()
257
+
258
+ def __str__(self) -> str:
259
+ return self.to_str()
260
+
261
+ def __repr__(self) -> str:
262
+ return f'<DeltaSplit {self.to_str(max_number_of_fields=7, directionals=("{}", "{}, is_past=True"), no_time_str="0", include_seconds=True, granularity_halting_threshold=None)}>'
263
+
264
+ def to_dict(self) -> Dict:
265
+ parts = {'largest': self.largest_field_name,
266
+ 'second_largest': self.second_largest_field_name,
267
+ 'is_past': self.is_past,
268
+ 'days': self.days,
269
+ 'hours': self.hours,
270
+ 'minutes': self.minutes,
271
+ 'seconds': self.seconds}
272
+ if self.years:
273
+ parts['years'] = self.years
274
+ if self.months:
275
+ parts['months'] = self.months
276
+ if self.weeks:
277
+ parts['weeks'] = self.weeks
278
+ return parts
@@ -0,0 +1,24 @@
1
+ __all__ = [
2
+ 'T_DATETIME_VALUE',
3
+ 'T_DATE_VALUE',
4
+ 'T_TIME_VALUE',
5
+ 'T_TEMPORAL_VALUE',
6
+ 'T_NUMBER',
7
+ ]
8
+
9
+ from batutils.dtu.structs._base import *
10
+
11
+ # Any datetime value
12
+ T_DATETIME_VALUE = Union[datetime.datetime, datetime.date, datetime.time]
13
+
14
+ # Containing a date
15
+ T_DATE_VALUE = Union[datetime.datetime, datetime.date]
16
+
17
+ # Containing time
18
+ T_TIME_VALUE = Union[datetime.datetime, datetime.time]
19
+
20
+ # Any type that can possibly contain a date and/or time value
21
+ T_TEMPORAL_VALUE = Union[T_DATETIME_VALUE, float, int, str, bytes]
22
+
23
+ # Number
24
+ T_NUMBER = Union[int, float]
@@ -0,0 +1,21 @@
1
+ __all__ = [
2
+ 'Date',
3
+ 'Time',
4
+ 'Datetime',
5
+ 'TimeDelta',
6
+ 'TzInfo',
7
+ 'TimeZone',
8
+ ]
9
+ from batutils.dtu.structs._base import *
10
+
11
+ # Aliases to "standard" PEP 8 class names. This is to increase readability and
12
+ # reduce confusion related to the fact that the most commonly used class/object
13
+ # shared the name of its own module (i.e. `datetime.datetime`)
14
+ Date = datetime.date
15
+ Time = datetime.time
16
+ Datetime = datetime.datetime
17
+ TimeDelta = datetime.timedelta
18
+ TzInfo = datetime.tzinfo
19
+ TimeZone = datetime.timezone
20
+
21
+
@@ -0,0 +1,35 @@
1
+ __all__ = [
2
+ 'DAYS_IN_MEAN_YEAR',
3
+ 'DAYS_IN_MEAN_MONTH',
4
+ 'DAYS_IN_MEDIAN_YEAR',
5
+ 'DAYS_IN_MEDIAN_MONTH',
6
+ 'WHOLE_DAYS_IN_MEAN_YEAR',
7
+ 'WHOLE_DAYS_IN_MEAN_MONTH',
8
+ 'SECONDS_IN_ONE_MINUTE',
9
+ 'SECONDS_IN_ONE_HOUR',
10
+ 'SECONDS_IN_ONE_DAY',
11
+ 'Weekday',
12
+ ]
13
+
14
+ import enum
15
+
16
+ DAYS_IN_MEAN_YEAR = 365.2425 # Every 400 years we get 97 leap years (303*365 + 97*366)/400
17
+ DAYS_IN_MEAN_MONTH = 30.436875 # Mean Year / 12
18
+ DAYS_IN_MEDIAN_YEAR = 365 # Most common value (303 vs. 97 per 400 year cycle)
19
+ DAYS_IN_MEDIAN_MONTH = 31 # Most common value (7 out of 12)
20
+ WHOLE_DAYS_IN_MEAN_YEAR = 365
21
+ WHOLE_DAYS_IN_MEAN_MONTH = 30
22
+
23
+ SECONDS_IN_ONE_MINUTE = 60
24
+ SECONDS_IN_ONE_HOUR = 3600 # 60 * 60
25
+ SECONDS_IN_ONE_DAY = 86400 # 60 * 60 * 24
26
+
27
+
28
+ class Weekday(enum.IntEnum):
29
+ MONDAY = 0
30
+ TUESDAY = 1
31
+ WEDNESDAY = 2
32
+ THURSDAY = 3
33
+ FRIDAY = 4
34
+ SATURDAY = 5
35
+ SUNDAY = 6
@@ -0,0 +1,7 @@
1
+ from batutils.structs._base import *
2
+
3
+ from batutils.tpu.structs.empty import *
4
+ from batutils.tpu.structs.singleton import *
5
+ from batutils.tpu.structs.enumex import *
6
+
7
+ from batutils.dtu.structs.aliases import *