Mercurial > libervia-backend
comparison sat/tools/common/date_utils.py @ 3905:92482cc80d0b
tools (common/date_utils) handle timestamp and `in` + `delta2human`:
regex used to parse datetimes has been improved to handle a unix time (which can be used
with `+ <delta>` or `- <delta>`), and the `in <delta>` (e.g. `in 6 days`).
`DEFAULT_DATETIME` has been removed as dateutil use current date by default, which is the
expected behaviour.
Add `delta2human` method which convert the difference of 2 unix times to a human friendly
approximate text.
rel 372
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 22 Sep 2022 00:01:48 +0200 |
parents | bef32f3ccc06 |
children | 883db2790b11 |
comparison
equal
deleted
inserted
replaced
3904:0aa7023dcd08 | 3905:92482cc80d0b |
---|---|
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 """tools to help manipulating time and dates""" | 20 """tools to help manipulating time and dates""" |
21 | 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 sat.core import exceptions | |
22 from sat.core.constants import Const as C | 35 from sat.core.constants import Const as C |
23 from sat.core.i18n import _ | 36 from sat.core.i18n import _ |
24 import datetime | 37 |
25 from dateutil import tz, parser | 38 RELATIVE_RE = re.compile( |
26 from dateutil.relativedelta import relativedelta | 39 r"\s*(?P<in>\bin\b)?" |
27 from dateutil.utils import default_tzinfo | 40 r"(?P<date>[^+-].+[^\s+-])?\s*(?P<direction>[-+])?\s*" |
28 from dateutil.parser import ParserError | 41 r"\s*(?P<quantity>\d+)\s*" |
29 from babel import dates | 42 r"(?P<unit>(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d|week|w|year|yr|y))s?" |
30 import calendar | 43 r"(?P<ago>\s+ago)?\s*", |
31 import time | 44 re.I |
32 import re | 45 ) |
33 | |
34 RELATIVE_RE = re.compile(r"(?P<date>.*?)(?P<direction>[-+]?) *(?P<quantity>\d+) *" | |
35 r"(?P<unit>(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d" | |
36 r"|week|w|year|yr|y))s?" | |
37 r"(?P<ago> +ago)?", re.I) | |
38 TIME_SYMBOL_MAP = { | 46 TIME_SYMBOL_MAP = { |
39 "s": "second", | 47 "s": "second", |
40 "sec": "second", | 48 "sec": "second", |
41 "m": "minute", | 49 "m": "minute", |
42 "min": "minute", | 50 "min": "minute", |
49 "yr": "year", | 57 "yr": "year", |
50 } | 58 } |
51 YEAR_FIRST_RE = re.compile(r"\d{4}[^\d]+") | 59 YEAR_FIRST_RE = re.compile(r"\d{4}[^\d]+") |
52 TZ_UTC = tz.tzutc() | 60 TZ_UTC = tz.tzutc() |
53 TZ_LOCAL = tz.gettz() | 61 TZ_LOCAL = tz.gettz() |
54 # used to replace values when something is missing | |
55 DEFAULT_DATETIME = datetime.datetime(2000, 0o1, 0o1) | |
56 | 62 |
57 | 63 |
58 def date_parse(value, default_tz=TZ_UTC): | 64 def date_parse(value, default_tz=TZ_UTC): |
59 """Parse a date and return corresponding unix timestamp | 65 """Parse a date and return corresponding unix timestamp |
60 | 66 |
65 value = str(value).strip() | 71 value = str(value).strip() |
66 dayfirst = False if YEAR_FIRST_RE.match(value) else True | 72 dayfirst = False if YEAR_FIRST_RE.match(value) else True |
67 | 73 |
68 try: | 74 try: |
69 dt = default_tzinfo( | 75 dt = default_tzinfo( |
70 parser.parse(value, default=DEFAULT_DATETIME, dayfirst=dayfirst), | 76 parser.parse(value, dayfirst=dayfirst), |
71 default_tz) | 77 default_tz) |
72 except ParserError as e: | 78 except ParserError as e: |
73 if value == "now": | 79 if value == "now": |
74 dt = datetime.datetime.now(tz.tzutc()) | 80 dt = datetime.datetime.now(tz.tzutc()) |
75 else: | 81 else: |
76 raise e | 82 try: |
83 # the date may already be a timestamp | |
84 return int(value) | |
85 except ValueError: | |
86 raise e | |
77 return calendar.timegm(dt.utctimetuple()) | 87 return calendar.timegm(dt.utctimetuple()) |
78 | 88 |
79 def date_parse_ext(value, default_tz=TZ_UTC): | 89 def date_parse_ext(value, default_tz=TZ_UTC): |
80 """Extended date parse which accept relative date | 90 """Extended date parse which accept relative date |
81 | 91 |
90 """ | 100 """ |
91 m = RELATIVE_RE.match(value) | 101 m = RELATIVE_RE.match(value) |
92 if m is None: | 102 if m is None: |
93 return date_parse(value, default_tz=default_tz) | 103 return date_parse(value, default_tz=default_tz) |
94 | 104 |
95 if m.group("direction") and m.group("ago"): | 105 if sum(1 for g in ("direction", "in", "ago") if m.group(g)) > 1: |
96 raise ValueError( | 106 raise ValueError( |
97 _("You can't use a direction (+ or -) and \"ago\" at the same time")) | 107 _('You can use only one of direction (+ or -), "in" and "ago"')) |
98 | 108 |
99 if m.group("direction") == '-' or m.group("ago"): | 109 if m.group("direction") == '-' or m.group("ago"): |
100 direction = -1 | 110 direction = -1 |
101 else: | 111 else: |
102 direction = 1 | 112 direction = 1 |
103 | 113 |
104 date = m.group("date").strip().lower() | 114 date = m.group("date") |
115 if date is not None: | |
116 date = date.strip() | |
105 if not date or date == "now": | 117 if not date or date == "now": |
106 dt = datetime.datetime.now(tz.tzutc()) | 118 dt = datetime.datetime.now(tz.tzutc()) |
107 else: | 119 else: |
108 dt = default_tzinfo(parser.parse(date, dayfirst=True), default_tz) | 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()) | |
109 | 129 |
110 quantity = int(m.group("quantity")) | 130 quantity = int(m.group("quantity")) |
111 unit = m.group("unit").lower() | 131 unit = m.group("unit").lower() |
112 try: | 132 try: |
113 unit = TIME_SYMBOL_MAP[unit] | 133 unit = TIME_SYMBOL_MAP[unit] |
189 fmt = "yyyy-MM-ddTHH:mm:ss'Z'" | 209 fmt = "yyyy-MM-ddTHH:mm:ss'Z'" |
190 return dates.format_datetime(timestamp, format=fmt) | 210 return dates.format_datetime(timestamp, format=fmt) |
191 else: | 211 else: |
192 return dates.format_datetime(timestamp, format=fmt, locale=locale_str, | 212 return dates.format_datetime(timestamp, format=fmt, locale=locale_str, |
193 tzinfo=tz_info) | 213 tzinfo=tz_info) |
214 | |
215 | |
216 def delta2human(start_ts: Union[float, int], end_ts: Union[float, int]) -> str: | |
217 """Convert delta of 2 unix times to human readable text | |
218 | |
219 @param start_ts: timestamp of starting time | |
220 @param end_ts: timestamp of ending time | |
221 """ | |
222 if end_ts < start_ts: | |
223 raise exceptions.InternalError( | |
224 "end timestamp must be bigger or equal to start timestamp !" | |
225 ) | |
226 rd = relativedelta( | |
227 datetime.datetime.fromtimestamp(end_ts), | |
228 datetime.datetime.fromtimestamp(start_ts) | |
229 ) | |
230 text_elems = [] | |
231 for unit in ("years", "months", "days", "hours", "minutes"): | |
232 value = getattr(rd, unit) | |
233 if value == 1: | |
234 # we remove final "s" when there is only 1 | |
235 text_elems.append(f"1 {unit[:-1]}") | |
236 elif value > 1: | |
237 text_elems.append(f"{value} {unit}") | |
238 | |
239 return ", ".join(text_elems) |