+) -> Tuple[datetime.date, datetime.time]:
+ """Return the component date and time objects of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> (d, t) = datetime_to_date_and_time(dt)
+ >>> d
+ datetime.date(2021, 12, 25)
+ >>> t
+ datetime.time(12, 30)
+
+ """
+ return (dt.date(), dt.timetz())
+
+
+def datetime_to_date(dt: datetime.datetime) -> datetime.date:
+ """Return the date part of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> datetime_to_date(dt)
+ datetime.date(2021, 12, 25)
+
+ """
+ return datetime_to_date_and_time(dt)[0]
+
+
+def datetime_to_time(dt: datetime.datetime) -> datetime.time:
+ """Return the time part of a datetime.
+
+ >>> import datetime
+ >>> dt = datetime.datetime(2021, 12, 25, 12, 30)
+ >>> datetime_to_time(dt)
+ datetime.time(12, 30)
+
+ """
+ return datetime_to_date_and_time(dt)[1]
+
+
+class TimeUnit(enum.IntEnum):
+ """An enum to represent units with which we can compute deltas."""
+
+ MONDAYS = 0
+ TUESDAYS = 1
+ WEDNESDAYS = 2
+ THURSDAYS = 3
+ FRIDAYS = 4
+ SATURDAYS = 5
+ SUNDAYS = 6
+ SECONDS = 7
+ MINUTES = 8
+ HOURS = 9
+ DAYS = 10
+ WORKDAYS = 11
+ WEEKS = 12
+ MONTHS = 13
+ YEARS = 14
+
+ @classmethod
+ def is_valid(cls, value: Any):
+ if type(value) is int:
+ return value in cls._value2member_map_
+ elif type(value) is TimeUnit:
+ return value.value in cls._value2member_map_
+ elif type(value) is str:
+ return value in cls._member_names_
+ else:
+ print(type(value))
+ return False
+
+
+def n_timeunits_from_base(count: int, unit: TimeUnit, base: datetime.datetime) -> datetime.datetime:
+ """Return a datetime that is N units before/after a base datetime.
+ e.g. 3 Wednesdays from base datetime, 2 weeks from base date, 10
+ years before base datetime, 13 minutes after base datetime, etc...
+ Note: to indicate before/after the base date, use a positive or
+ negative count.
+
+ >>> base = string_to_datetime("2021/09/10 11:24:51AM-0700")[0]
+
+ The next (1) Monday from the base datetime:
+ >>> n_timeunits_from_base(+1, TimeUnit.MONDAYS, base)
+ datetime.datetime(2021, 9, 13, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Ten (10) years after the base datetime:
+ >>> n_timeunits_from_base(10, TimeUnit.YEARS, base)
+ datetime.datetime(2031, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) working days (M..F, not counting holidays) after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.WORKDAYS, base)
+ datetime.datetime(2021, 11, 23, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) days (including weekends and holidays) after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.DAYS, base)
+ datetime.datetime(2021, 10, 30, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) months before (note negative count) base datetime:
+ >>> n_timeunits_from_base(-50, TimeUnit.MONTHS, base)
+ datetime.datetime(2017, 7, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) hours after base datetime:
+ >>> n_timeunits_from_base(50, TimeUnit.HOURS, base)
+ datetime.datetime(2021, 9, 12, 13, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) minutes before base datetime:
+ >>> n_timeunits_from_base(-50, TimeUnit.MINUTES, base)
+ datetime.datetime(2021, 9, 10, 10, 34, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
+
+ Fifty (50) seconds from base datetime:
+ >>> 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:
+ return base
+
+ # N days from base
+ if unit == TimeUnit.DAYS:
+ timedelta = datetime.timedelta(days=count)
+ return base + timedelta
+
+ # N hours from base
+ elif unit == TimeUnit.HOURS:
+ timedelta = datetime.timedelta(hours=count)
+ return base + timedelta
+
+ # N minutes from base
+ elif unit == TimeUnit.MINUTES:
+ timedelta = datetime.timedelta(minutes=count)
+ return base + timedelta
+
+ # N seconds from base
+ elif unit == TimeUnit.SECONDS:
+ timedelta = datetime.timedelta(seconds=count)
+ return base + timedelta
+
+ # N workdays from base
+ elif unit == TimeUnit.WORKDAYS:
+ if count < 0:
+ count = abs(count)
+ timedelta = datetime.timedelta(days=-1)
+ else:
+ timedelta = datetime.timedelta(days=1)
+ skips = holidays.US(years=base.year).keys()
+ while count > 0:
+ old_year = base.year
+ base += timedelta
+ if base.year != old_year:
+ skips = holidays.US(years=base.year).keys()
+ if base.weekday() < 5 and datetime.date(base.year, base.month, base.day) not in skips:
+ count -= 1
+ return base
+
+ # N weeks from base
+ elif unit == TimeUnit.WEEKS:
+ timedelta = datetime.timedelta(weeks=count)
+ base = base + timedelta
+ return base
+
+ # N months from base
+ elif unit == TimeUnit.MONTHS:
+ month_term = count % 12
+ year_term = count // 12
+ new_month = base.month + month_term
+ if new_month > 12:
+ new_month %= 12
+ year_term += 1
+ new_year = base.year + year_term
+ 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:
+ new_year = base.year + count
+ return datetime.datetime(
+ new_year,
+ base.month,
+ base.day,
+ base.hour,
+ base.minute,
+ base.second,
+ base.microsecond,
+ base.tzinfo,
+ )
+
+ if unit not in set(
+ [
+ TimeUnit.MONDAYS,
+ TimeUnit.TUESDAYS,
+ TimeUnit.WEDNESDAYS,
+ TimeUnit.THURSDAYS,
+ TimeUnit.FRIDAYS,
+ TimeUnit.SATURDAYS,
+ TimeUnit.SUNDAYS,
+ ]
+ ):
+ raise ValueError(unit)
+
+ # N weekdays from base (e.g. 4 wednesdays from today)
+ direction = 1 if count > 0 else -1
+ count = abs(count)
+ timedelta = datetime.timedelta(days=direction)
+ start = base
+ while True:
+ dow = base.weekday()
+ if dow == unit.value and start != base:
+ count -= 1
+ if count == 0:
+ return base
+ base = base + timedelta
+
+
+def get_format_string(