+def now_pacific() -> datetime.datetime:
+ """
+ What time is it? Result in US/Pacific time (PST/PDT)
+ """
+ return datetime.datetime.now(pytz.timezone("US/Pacific"))
+
+
+def date_to_datetime(date: datetime.date) -> datetime.datetime:
+ """
+ Given a date, return a datetime with hour/min/sec zero (midnight)
+
+ >>> import datetime
+ >>> date_to_datetime(datetime.date(2021, 12, 25))
+ datetime.datetime(2021, 12, 25, 0, 0)
+
+ """
+ return datetime.datetime(
+ date.year,
+ date.month,
+ date.day,
+ 0, 0, 0, 0
+ )
+
+
+def time_to_datetime_today(time: datetime.time) -> datetime.datetime:
+ """
+ Given a time, returns that time as a datetime with a date component
+ set based on the current date. If the time passed is timezone aware,
+ the resulting datetime will also be (and will use the same tzinfo).
+ If the time is timezone naive, the datetime returned will be too.
+
+ >>> t = datetime.time(13, 14, 0)
+ >>> d = now_pacific().date()
+ >>> dt = time_to_datetime_today(t)
+ >>> dt.date() == d
+ True
+
+ >>> dt.time() == t
+ True
+
+ >>> dt.tzinfo == t.tzinfo
+ True
+
+ >>> dt.tzinfo == None
+ True
+
+ >>> t = datetime.time(8, 15, 12, 0, pytz.UTC)
+ >>> t.tzinfo == None
+ False
+
+ >>> dt = time_to_datetime_today(t)
+ >>> dt.tzinfo == None
+ False
+
+ """
+ now = now_pacific()
+ tz = time.tzinfo
+ return datetime.datetime.combine(now, time, tz)
+
+
+def date_and_time_to_datetime(date: datetime.date,
+ time: datetime.time) -> datetime.datetime:
+ """
+ Given a date and time, merge them and return a datetime.
+
+ >>> import datetime
+ >>> d = datetime.date(2021, 12, 25)
+ >>> t = datetime.time(12, 30, 0, 0)
+ >>> date_and_time_to_datetime(d, t)
+ datetime.datetime(2021, 12, 25, 12, 30)
+
+ """
+ return datetime.datetime(
+ date.year,
+ date.month,
+ date.day,
+ time.hour,
+ time.minute,
+ time.second,
+ time.microsecond,
+ )
+
+
+def datetime_to_date_and_time(
+ dt: datetime.datetime
+) -> 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.Enum):
+ """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)))
+
+ """
+ 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
+ return datetime.datetime(
+ new_year,
+ new_month,
+ base.day,
+ base.hour,
+ base.minute,
+ base.second,
+ base.microsecond,
+ base.tzinfo,
+ )
+
+ # 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(
+ *,
+ date_time_separator=" ",
+ include_timezone=True,
+ include_dayname=False,
+ use_month_abbrevs=False,
+ include_seconds=True,
+ include_fractional=False,
+ twelve_hour=True,