More type annotations.
[python_utils.git] / datetime_utils.py
index 97947203f6a126348d1b6602c320b07909f83970..6f504f6c304b850e830cffab08d0ed8be67fed39 100644 (file)
@@ -34,9 +34,7 @@ def is_timezone_naive(dt: datetime.datetime) -> bool:
     return not is_timezone_aware(dt)
 
 
-def replace_timezone(
-    dt: datetime.datetime, tz: datetime.tzinfo
-) -> datetime.datetime:
+def replace_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime:
     """
     Replaces the timezone on a datetime object directly (leaving
     the year, month, day, hour, minute, second, micro, etc... alone).
@@ -66,9 +64,7 @@ def replace_timezone(
     )
 
 
-def replace_time_timezone(
-    t: datetime.time, tz: datetime.tzinfo
-) -> datetime.time:
+def replace_time_timezone(t: datetime.time, tz: datetime.tzinfo) -> datetime.time:
     """
     Replaces the timezone on a datetime.time directly without performing
     any translation.
@@ -85,9 +81,7 @@ def replace_time_timezone(
     return t.replace(tzinfo=tz)
 
 
-def translate_timezone(
-    dt: datetime.datetime, tz: datetime.tzinfo
-) -> datetime.datetime:
+def translate_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime:
     """
     Translates dt into a different timezone by adjusting the year, month,
     day, hour, minute, second, micro, etc... appropriately.  The returned
@@ -310,6 +304,17 @@ def n_timeunits_from_base(
     >>> n_timeunits_from_base(50, TimeUnit.SECONDS, base)
     datetime.datetime(2021, 9, 10, 11, 25, 41, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
 
+    Next month corner case -- it will try to make Feb 31, 2022 then count
+    backwards.
+    >>> base = string_to_datetime("2022/01/31 11:24:51AM-0700")[0]
+    >>> n_timeunits_from_base(1, TimeUnit.MONTHS, base)
+    datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+    Last month with the same corner case
+    >>> base = string_to_datetime("2022/03/31 11:24:51AM-0700")[0]
+    >>> n_timeunits_from_base(-1, TimeUnit.MONTHS, base)
+    datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
     """
     assert TimeUnit.is_valid(unit)
     if count == 0:
@@ -370,16 +375,23 @@ def n_timeunits_from_base(
             new_month %= 12
             year_term += 1
         new_year = base.year + year_term
-        return datetime.datetime(
-            new_year,
-            new_month,
-            base.day,
-            base.hour,
-            base.minute,
-            base.second,
-            base.microsecond,
-            base.tzinfo,
-        )
+        day = base.day
+        while True:
+            try:
+                ret = datetime.datetime(
+                    new_year,
+                    new_month,
+                    day,
+                    base.hour,
+                    base.minute,
+                    base.second,
+                    base.microsecond,
+                    base.tzinfo,
+                )
+                break
+            except ValueError:
+                day -= 1
+        return ret
 
     # N years from base
     elif unit == TimeUnit.YEARS:
@@ -768,7 +780,7 @@ def describe_timedelta(delta: datetime.timedelta) -> str:
     '1 day, and 10 minutes'
 
     """
-    return describe_duration(delta.total_seconds())
+    return describe_duration(int(delta.total_seconds()))  # Note: drops milliseconds
 
 
 def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str:
@@ -813,7 +825,9 @@ def describe_timedelta_briefly(delta: datetime.timedelta) -> str:
     '1d 10m'
 
     """
-    return describe_duration_briefly(delta.total_seconds())
+    return describe_duration_briefly(
+        int(delta.total_seconds())
+    )  # Note: drops milliseconds
 
 
 if __name__ == '__main__':