# HG changeset patch # User Goffi # Date 1663797708 -7200 # Node ID 92482cc80d0b0ce128b5f23f7c9da0d78459e285 # Parent 0aa7023dcd083d2b840f9d7c09ab0847ce9c0ada 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 `+ ` or `- `), and the `in ` (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 diff -r 0aa7023dcd08 -r 92482cc80d0b sat/tools/common/date_utils.py --- a/sat/tools/common/date_utils.py Thu Sep 22 00:01:41 2022 +0200 +++ b/sat/tools/common/date_utils.py Thu Sep 22 00:01:48 2022 +0200 @@ -19,22 +19,30 @@ """tools to help manipulating time and dates""" -from sat.core.constants import Const as C -from sat.core.i18n import _ +from typing import Union +import calendar import datetime -from dateutil import tz, parser +import re +import time + +from babel import dates +from dateutil import parser, tz +from dateutil.parser import ParserError from dateutil.relativedelta import relativedelta from dateutil.utils import default_tzinfo -from dateutil.parser import ParserError -from babel import dates -import calendar -import time -import re + +from sat.core import exceptions +from sat.core.constants import Const as C +from sat.core.i18n import _ -RELATIVE_RE = re.compile(r"(?P.*?)(?P[-+]?) *(?P\d+) *" - r"(?P(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d" - r"|week|w|year|yr|y))s?" - r"(?P +ago)?", re.I) +RELATIVE_RE = re.compile( + r"\s*(?P\bin\b)?" + r"(?P[^+-].+[^\s+-])?\s*(?P[-+])?\s*" + r"\s*(?P\d+)\s*" + r"(?P(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d|week|w|year|yr|y))s?" + r"(?P\s+ago)?\s*", + re.I +) TIME_SYMBOL_MAP = { "s": "second", "sec": "second", @@ -51,8 +59,6 @@ YEAR_FIRST_RE = re.compile(r"\d{4}[^\d]+") TZ_UTC = tz.tzutc() TZ_LOCAL = tz.gettz() -# used to replace values when something is missing -DEFAULT_DATETIME = datetime.datetime(2000, 0o1, 0o1) def date_parse(value, default_tz=TZ_UTC): @@ -67,13 +73,17 @@ try: dt = default_tzinfo( - parser.parse(value, default=DEFAULT_DATETIME, dayfirst=dayfirst), + parser.parse(value, dayfirst=dayfirst), default_tz) except ParserError as e: if value == "now": dt = datetime.datetime.now(tz.tzutc()) else: - raise e + try: + # the date may already be a timestamp + return int(value) + except ValueError: + raise e return calendar.timegm(dt.utctimetuple()) def date_parse_ext(value, default_tz=TZ_UTC): @@ -92,20 +102,30 @@ if m is None: return date_parse(value, default_tz=default_tz) - if m.group("direction") and m.group("ago"): + if sum(1 for g in ("direction", "in", "ago") if m.group(g)) > 1: raise ValueError( - _("You can't use a direction (+ or -) and \"ago\" at the same time")) + _('You can use only one of direction (+ or -), "in" and "ago"')) if m.group("direction") == '-' or m.group("ago"): direction = -1 else: direction = 1 - date = m.group("date").strip().lower() + date = m.group("date") + if date is not None: + date = date.strip() if not date or date == "now": dt = datetime.datetime.now(tz.tzutc()) else: - dt = default_tzinfo(parser.parse(date, dayfirst=True), default_tz) + try: + dt = default_tzinfo(parser.parse(date, dayfirst=True), default_tz) + except ParserError as e: + try: + timestamp = int(date) + except ValueError: + raise e + else: + dt = datetime.datetime.fromtimestamp(timestamp, tz.tzutc()) quantity = int(m.group("quantity")) unit = m.group("unit").lower() @@ -191,3 +211,29 @@ else: return dates.format_datetime(timestamp, format=fmt, locale=locale_str, tzinfo=tz_info) + + +def delta2human(start_ts: Union[float, int], end_ts: Union[float, int]) -> str: + """Convert delta of 2 unix times to human readable text + + @param start_ts: timestamp of starting time + @param end_ts: timestamp of ending time + """ + if end_ts < start_ts: + raise exceptions.InternalError( + "end timestamp must be bigger or equal to start timestamp !" + ) + rd = relativedelta( + datetime.datetime.fromtimestamp(end_ts), + datetime.datetime.fromtimestamp(start_ts) + ) + text_elems = [] + for unit in ("years", "months", "days", "hours", "minutes"): + value = getattr(rd, unit) + if value == 1: + # we remove final "s" when there is only 1 + text_elems.append(f"1 {unit[:-1]}") + elif value > 1: + text_elems.append(f"{value} {unit}") + + return ", ".join(text_elems)