Mercurial > libervia-backend
comparison libervia/backend/tools/common/date_utils.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/tools/common/date_utils.py@432f7e422a27 |
children | 4325a0f13b0f |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # SAT: a jabber client | |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 """tools to help manipulating time and dates""" | |
21 | |
22 from typing import Union | |
23 import calendar | |
24 import datetime | |
25 import re | |
26 import time | |
27 | |
28 from babel import dates | |
29 from dateutil import parser, tz | |
30 from dateutil.parser import ParserError | |
31 from dateutil.relativedelta import relativedelta | |
32 from dateutil.utils import default_tzinfo | |
33 | |
34 from libervia.backend.core import exceptions | |
35 from libervia.backend.core.constants import Const as C | |
36 from libervia.backend.core.i18n import _ | |
37 | |
38 RELATIVE_RE = re.compile( | |
39 r"\s*(?P<in>\bin\b)?" | |
40 r"(?P<date>[^+-].+[^\s+-])?\s*(?P<direction>[-+])?\s*" | |
41 r"\s*(?P<quantity>\d+)\s*" | |
42 r"(?P<unit>(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d|week|w|year|yr|y))s?" | |
43 r"(?P<ago>\s+ago)?\s*", | |
44 re.I | |
45 ) | |
46 TIME_SYMBOL_MAP = { | |
47 "s": "second", | |
48 "sec": "second", | |
49 "m": "minute", | |
50 "min": "minute", | |
51 "h": "hour", | |
52 "hr": "hour", | |
53 "d": "day", | |
54 "w": "week", | |
55 "mo": "month", | |
56 "y": "year", | |
57 "yr": "year", | |
58 } | |
59 YEAR_FIRST_RE = re.compile(r"\d{4}[^\d]+") | |
60 TZ_UTC = tz.tzutc() | |
61 TZ_LOCAL = tz.gettz() | |
62 | |
63 | |
64 def date_parse(value, default_tz=TZ_UTC): | |
65 """Parse a date and return corresponding unix timestamp | |
66 | |
67 @param value(unicode): date to parse, in any format supported by parser | |
68 @param default_tz(datetime.tzinfo): default timezone | |
69 @return (int): timestamp | |
70 """ | |
71 value = str(value).strip() | |
72 dayfirst = False if YEAR_FIRST_RE.match(value) else True | |
73 | |
74 try: | |
75 dt = default_tzinfo( | |
76 parser.parse(value, dayfirst=dayfirst), | |
77 default_tz) | |
78 except ParserError as e: | |
79 if value == "now": | |
80 dt = datetime.datetime.now(tz.tzutc()) | |
81 else: | |
82 try: | |
83 # the date may already be a timestamp | |
84 return int(value) | |
85 except ValueError: | |
86 raise e | |
87 return calendar.timegm(dt.utctimetuple()) | |
88 | |
89 def date_parse_ext(value, default_tz=TZ_UTC): | |
90 """Extended date parse which accept relative date | |
91 | |
92 @param value(unicode): date to parse, in any format supported by parser | |
93 and with the hability to specify X days/weeks/months/years in the past or future. | |
94 Relative date are specified either with something like `[main_date] +1 week` | |
95 or with something like `3 days ago`, and it is case insensitive. [main_date] is | |
96 a date parsable by parser, or empty to specify current date/time. | |
97 "now" can also be used to specify current date/time. | |
98 @param default_tz(datetime.tzinfo): same as for date_parse | |
99 @return (int): timestamp | |
100 """ | |
101 m = RELATIVE_RE.match(value) | |
102 if m is None: | |
103 return date_parse(value, default_tz=default_tz) | |
104 | |
105 if sum(1 for g in ("direction", "in", "ago") if m.group(g)) > 1: | |
106 raise ValueError( | |
107 _('You can use only one of direction (+ or -), "in" and "ago"')) | |
108 | |
109 if m.group("direction") == '-' or m.group("ago"): | |
110 direction = -1 | |
111 else: | |
112 direction = 1 | |
113 | |
114 date = m.group("date") | |
115 if date is not None: | |
116 date = date.strip() | |
117 if not date or date == "now": | |
118 dt = datetime.datetime.now(tz.tzutc()) | |
119 else: | |
120 try: | |
121 dt = default_tzinfo(parser.parse(date, dayfirst=True), default_tz) | |
122 except ParserError as e: | |
123 try: | |
124 timestamp = int(date) | |
125 except ValueError: | |
126 raise e | |
127 else: | |
128 dt = datetime.datetime.fromtimestamp(timestamp, tz.tzutc()) | |
129 | |
130 quantity = int(m.group("quantity")) | |
131 unit = m.group("unit").lower() | |
132 try: | |
133 unit = TIME_SYMBOL_MAP[unit] | |
134 except KeyError: | |
135 pass | |
136 delta_kw = {f"{unit}s": direction * quantity} | |
137 dt = dt + relativedelta(**delta_kw) | |
138 return calendar.timegm(dt.utctimetuple()) | |
139 | |
140 | |
141 def date_fmt( | |
142 timestamp: Union[float, int, str], | |
143 fmt: str = "short", | |
144 date_only: bool = False, | |
145 auto_limit: int = 7, | |
146 auto_old_fmt: str = "short", | |
147 auto_new_fmt: str = "relative", | |
148 locale_str: str = C.DEFAULT_LOCALE, | |
149 tz_info: Union[datetime.tzinfo, str] = TZ_UTC | |
150 ) -> str: | |
151 """Format date according to locale | |
152 | |
153 @param timestamp: unix time | |
154 @param fmt: one of: | |
155 - short: e.g. u'31/12/17' | |
156 - medium: e.g. u'Apr 1, 2007' | |
157 - long: e.g. u'April 1, 2007' | |
158 - full: e.g. u'Sunday, April 1, 2007' | |
159 - relative: format in relative time | |
160 e.g.: 3 hours | |
161 note that this format is not precise | |
162 - iso: ISO 8601 format | |
163 e.g.: u'2007-04-01T19:53:23Z' | |
164 - auto: use auto_old_fmt if date is older than auto_limit | |
165 else use auto_new_fmt | |
166 - auto_day: shorcut to set auto format with change on day | |
167 old format will be short, and new format will be time only | |
168 or a free value which is passed to babel.dates.format_datetime | |
169 (see http://babel.pocoo.org/en/latest/dates.html?highlight=pattern#pattern-syntax) | |
170 @param date_only: if True, only display date (not datetime) | |
171 @param auto_limit: limit in days before using auto_old_fmt | |
172 use 0 to have a limit at last midnight (day change) | |
173 @param auto_old_fmt: format to use when date is older than limit | |
174 @param auto_new_fmt: format to use when date is equal to or more recent | |
175 than limit | |
176 @param locale_str: locale to use (as understood by babel) | |
177 @param tz_info: time zone to use | |
178 | |
179 """ | |
180 timestamp = float(timestamp) | |
181 if isinstance(tz_info, str): | |
182 tz_info = tz.gettz(tz_info) | |
183 if fmt == "auto_day": | |
184 fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm" | |
185 if fmt == "auto": | |
186 if auto_limit == 0: | |
187 now = datetime.datetime.now(tz_info) | |
188 # we want to use given tz_info, so we don't use date() or today() | |
189 today = datetime.datetime(year=now.year, month=now.month, day=now.day, | |
190 tzinfo=now.tzinfo) | |
191 today = calendar.timegm(today.utctimetuple()) | |
192 if timestamp < today: | |
193 fmt = auto_old_fmt | |
194 else: | |
195 fmt = auto_new_fmt | |
196 else: | |
197 days_delta = (time.time() - timestamp) / 3600 | |
198 if days_delta > (auto_limit or 7): | |
199 fmt = auto_old_fmt | |
200 else: | |
201 fmt = auto_new_fmt | |
202 | |
203 if fmt == "relative": | |
204 delta = timestamp - time.time() | |
205 return dates.format_timedelta( | |
206 delta, granularity="minute", add_direction=True, locale=locale_str | |
207 ) | |
208 elif fmt in ("short", "long", "full"): | |
209 if date_only: | |
210 dt = datetime.datetime.fromtimestamp(timestamp, tz_info) | |
211 return dates.format_date(dt, format=fmt, locale=locale_str) | |
212 else: | |
213 return dates.format_datetime(timestamp, format=fmt, locale=locale_str, | |
214 tzinfo=tz_info) | |
215 elif fmt == "iso": | |
216 if date_only: | |
217 fmt = "yyyy-MM-dd" | |
218 else: | |
219 fmt = "yyyy-MM-ddTHH:mm:ss'Z'" | |
220 return dates.format_datetime(timestamp, format=fmt) | |
221 else: | |
222 return dates.format_datetime(timestamp, format=fmt, locale=locale_str, | |
223 tzinfo=tz_info) | |
224 | |
225 | |
226 def delta2human(start_ts: Union[float, int], end_ts: Union[float, int]) -> str: | |
227 """Convert delta of 2 unix times to human readable text | |
228 | |
229 @param start_ts: timestamp of starting time | |
230 @param end_ts: timestamp of ending time | |
231 """ | |
232 if end_ts < start_ts: | |
233 raise exceptions.InternalError( | |
234 "end timestamp must be bigger or equal to start timestamp !" | |
235 ) | |
236 rd = relativedelta( | |
237 datetime.datetime.fromtimestamp(end_ts), | |
238 datetime.datetime.fromtimestamp(start_ts) | |
239 ) | |
240 text_elems = [] | |
241 for unit in ("years", "months", "days", "hours", "minutes"): | |
242 value = getattr(rd, unit) | |
243 if value == 1: | |
244 # we remove final "s" when there is only 1 | |
245 text_elems.append(f"1 {unit[:-1]}") | |
246 elif value > 1: | |
247 text_elems.append(f"{value} {unit}") | |
248 | |
249 return ", ".join(text_elems) | |
250 | |
251 | |
252 def get_timezone_name(tzinfo, timestamp: Union[float, int]) -> str: | |
253 """ | |
254 Get the DST-aware timezone name for a given timezone and timestamp. | |
255 | |
256 @param tzinfo: The timezone to get the name for | |
257 @param timestamp: The timestamp to use, as a Unix timestamp (number of seconds since | |
258 the Unix epoch). | |
259 @return: The DST-aware timezone name. | |
260 """ | |
261 | |
262 dt = datetime.datetime.fromtimestamp(timestamp) | |
263 dt_tz = dt.replace(tzinfo=tzinfo) | |
264 tz_name = dt_tz.tzname() | |
265 if tz_name is None: | |
266 raise exceptions.InternalError("tz_name should not be None") | |
267 return tz_name |