changeset 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 0aa7023dcd08
children d8baf92cb921
files sat/tools/common/date_utils.py
diffstat 1 files changed, 66 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- 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<date>.*?)(?P<direction>[-+]?) *(?P<quantity>\d+) *"
-                         r"(?P<unit>(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d"
-                         r"|week|w|year|yr|y))s?"
-                         r"(?P<ago> +ago)?", re.I)
+RELATIVE_RE = re.compile(
+    r"\s*(?P<in>\bin\b)?"
+    r"(?P<date>[^+-].+[^\s+-])?\s*(?P<direction>[-+])?\s*"
+    r"\s*(?P<quantity>\d+)\s*"
+    r"(?P<unit>(second|sec|s|minute|min|month|mo|m|hour|hr|h|day|d|week|w|year|yr|y))s?"
+    r"(?P<ago>\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)